feat(runtime-core): type and attr fallthrough support for emits option
This commit is contained in:
parent
c409d4f297
commit
bf473a64ea
@ -8,7 +8,8 @@ import {
|
||||
onUpdated,
|
||||
defineComponent,
|
||||
openBlock,
|
||||
createBlock
|
||||
createBlock,
|
||||
FunctionalComponent
|
||||
} from '@vue/runtime-dom'
|
||||
import { mockWarn } from '@vue/shared'
|
||||
|
||||
@ -428,4 +429,70 @@ describe('attribute fallthrough', () => {
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe(`<div aria-hidden="false" class="barr"></div>`)
|
||||
})
|
||||
|
||||
it('should not let listener fallthrough when declared in emits (stateful)', () => {
|
||||
const Child = defineComponent({
|
||||
emits: ['click'],
|
||||
render() {
|
||||
return h(
|
||||
'button',
|
||||
{
|
||||
onClick: () => {
|
||||
this.$emit('click', 'custom')
|
||||
}
|
||||
},
|
||||
'hello'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const onClick = jest.fn()
|
||||
const App = {
|
||||
render() {
|
||||
return h(Child, {
|
||||
onClick
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
render(h(App), root)
|
||||
|
||||
const node = root.children[0] as HTMLElement
|
||||
node.dispatchEvent(new CustomEvent('click'))
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
expect(onClick).toHaveBeenCalledWith('custom')
|
||||
})
|
||||
|
||||
it('should not let listener fallthrough when declared in emits (functional)', () => {
|
||||
const Child: FunctionalComponent<{}, { click: any }> = (_, { emit }) => {
|
||||
// should not be in props
|
||||
expect((_ as any).onClick).toBeUndefined()
|
||||
return h('button', {
|
||||
onClick: () => {
|
||||
emit('click', 'custom')
|
||||
}
|
||||
})
|
||||
}
|
||||
Child.emits = ['click']
|
||||
|
||||
const onClick = jest.fn()
|
||||
const App = {
|
||||
render() {
|
||||
return h(Child, {
|
||||
onClick
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
render(h(App), root)
|
||||
|
||||
const node = root.children[0] as HTMLElement
|
||||
node.dispatchEvent(new CustomEvent('click'))
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
expect(onClick).toHaveBeenCalledWith('custom')
|
||||
})
|
||||
})
|
||||
|
@ -3,7 +3,8 @@ import {
|
||||
MethodOptions,
|
||||
ComponentOptionsWithoutProps,
|
||||
ComponentOptionsWithArrayProps,
|
||||
ComponentOptionsWithObjectProps
|
||||
ComponentOptionsWithObjectProps,
|
||||
EmitsOptions
|
||||
} from './apiOptions'
|
||||
import { SetupContext, RenderFunction } from './component'
|
||||
import { ComponentPublicInstance } from './componentProxy'
|
||||
@ -39,13 +40,15 @@ export function defineComponent<Props, RawBindings = object>(
|
||||
// (uses user defined props interface)
|
||||
// return type is for Vetur and TSX support
|
||||
export function defineComponent<
|
||||
Props,
|
||||
RawBindings,
|
||||
D,
|
||||
Props = {},
|
||||
RawBindings = {},
|
||||
D = {},
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {}
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = Record<string, any>,
|
||||
EE extends string = string
|
||||
>(
|
||||
options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M>
|
||||
options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M, E, EE>
|
||||
): {
|
||||
new (): ComponentPublicInstance<
|
||||
Props,
|
||||
@ -53,6 +56,7 @@ export function defineComponent<
|
||||
D,
|
||||
C,
|
||||
M,
|
||||
E,
|
||||
VNodeProps & Props
|
||||
>
|
||||
}
|
||||
@ -65,12 +69,22 @@ export function defineComponent<
|
||||
RawBindings,
|
||||
D,
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {}
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = Record<string, any>,
|
||||
EE extends string = string
|
||||
>(
|
||||
options: ComponentOptionsWithArrayProps<PropNames, RawBindings, D, C, M>
|
||||
options: ComponentOptionsWithArrayProps<
|
||||
PropNames,
|
||||
RawBindings,
|
||||
D,
|
||||
C,
|
||||
M,
|
||||
E,
|
||||
EE
|
||||
>
|
||||
): {
|
||||
// array props technically doesn't place any contraints on props in TSX
|
||||
new (): ComponentPublicInstance<VNodeProps, RawBindings, D, C, M>
|
||||
new (): ComponentPublicInstance<VNodeProps, RawBindings, D, C, M, E>
|
||||
}
|
||||
|
||||
// overload 4: object format with object props declaration
|
||||
@ -82,9 +96,19 @@ export function defineComponent<
|
||||
RawBindings,
|
||||
D,
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {}
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = Record<string, any>,
|
||||
EE extends string = string
|
||||
>(
|
||||
options: ComponentOptionsWithObjectProps<PropsOptions, RawBindings, D, C, M>
|
||||
options: ComponentOptionsWithObjectProps<
|
||||
PropsOptions,
|
||||
RawBindings,
|
||||
D,
|
||||
C,
|
||||
M,
|
||||
E,
|
||||
EE
|
||||
>
|
||||
): {
|
||||
new (): ComponentPublicInstance<
|
||||
ExtractPropTypes<PropsOptions>,
|
||||
@ -92,6 +116,7 @@ export function defineComponent<
|
||||
D,
|
||||
C,
|
||||
M,
|
||||
E,
|
||||
VNodeProps & ExtractPropTypes<PropsOptions, false>
|
||||
>
|
||||
}
|
||||
|
@ -50,12 +50,14 @@ export interface ComponentOptionsBase<
|
||||
RawBindings,
|
||||
D,
|
||||
C extends ComputedOptions,
|
||||
M extends MethodOptions
|
||||
> extends LegacyOptions<Props, RawBindings, D, C, M>, SFCInternalOptions {
|
||||
M extends MethodOptions,
|
||||
E extends EmitsOptions,
|
||||
EE extends string = string
|
||||
> extends LegacyOptions<Props, D, C, M>, SFCInternalOptions {
|
||||
setup?: (
|
||||
this: void,
|
||||
props: Props,
|
||||
ctx: SetupContext
|
||||
ctx: SetupContext<E>
|
||||
) => RawBindings | RenderFunction | void
|
||||
name?: string
|
||||
template?: string | object // can be a direct DOM node
|
||||
@ -75,6 +77,7 @@ export interface ComponentOptionsBase<
|
||||
components?: Record<string, PublicAPIComponent>
|
||||
directives?: Record<string, Directive>
|
||||
inheritAttrs?: boolean
|
||||
emits?: E | EE[]
|
||||
|
||||
// Internal ------------------------------------------------------------------
|
||||
|
||||
@ -97,10 +100,14 @@ export type ComponentOptionsWithoutProps<
|
||||
RawBindings = {},
|
||||
D = {},
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {}
|
||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = Record<string, any>,
|
||||
EE extends string = string
|
||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
|
||||
props?: undefined
|
||||
} & ThisType<ComponentPublicInstance<{}, RawBindings, D, C, M, Readonly<Props>>>
|
||||
} & ThisType<
|
||||
ComponentPublicInstance<{}, RawBindings, D, C, M, E, Readonly<Props>>
|
||||
>
|
||||
|
||||
export type ComponentOptionsWithArrayProps<
|
||||
PropNames extends string = string,
|
||||
@ -108,10 +115,12 @@ export type ComponentOptionsWithArrayProps<
|
||||
D = {},
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = Record<string, any>,
|
||||
EE extends string = string,
|
||||
Props = Readonly<{ [key in PropNames]?: any }>
|
||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
|
||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
|
||||
props: PropNames[]
|
||||
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
|
||||
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
|
||||
|
||||
export type ComponentOptionsWithObjectProps<
|
||||
PropsOptions = ComponentObjectPropsOptions,
|
||||
@ -119,10 +128,12 @@ export type ComponentOptionsWithObjectProps<
|
||||
D = {},
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = Record<string, any>,
|
||||
EE extends string = string,
|
||||
Props = Readonly<ExtractPropTypes<PropsOptions>>
|
||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
|
||||
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
|
||||
props: PropsOptions
|
||||
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
|
||||
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
|
||||
|
||||
export type ComponentOptions =
|
||||
| ComponentOptionsWithoutProps<any, any, any, any, any>
|
||||
@ -138,6 +149,8 @@ export interface MethodOptions {
|
||||
[key: string]: Function
|
||||
}
|
||||
|
||||
export type EmitsOptions = Record<string, any> | string[]
|
||||
|
||||
export type ExtractComputedReturns<T extends any> = {
|
||||
[key in keyof T]: T[key] extends { get: Function }
|
||||
? ReturnType<T[key]['get']>
|
||||
@ -162,7 +175,6 @@ type ComponentInjectOptions =
|
||||
|
||||
export interface LegacyOptions<
|
||||
Props,
|
||||
RawBindings,
|
||||
D,
|
||||
C extends ComputedOptions,
|
||||
M extends MethodOptions
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
} from './errorHandling'
|
||||
import { AppContext, createAppContext, AppConfig } from './apiCreateApp'
|
||||
import { Directive, validateDirectiveName } from './directives'
|
||||
import { applyOptions, ComponentOptions } from './apiOptions'
|
||||
import { applyOptions, ComponentOptions, EmitsOptions } from './apiOptions'
|
||||
import {
|
||||
EMPTY_OBJ,
|
||||
isFunction,
|
||||
@ -52,9 +52,13 @@ export interface SFCInternalOptions {
|
||||
__hmrUpdated?: boolean
|
||||
}
|
||||
|
||||
export interface FunctionalComponent<P = {}> extends SFCInternalOptions {
|
||||
(props: P, ctx: SetupContext): VNodeChild
|
||||
export interface FunctionalComponent<
|
||||
P = {},
|
||||
E extends EmitsOptions = Record<string, any>
|
||||
> extends SFCInternalOptions {
|
||||
(props: P, ctx: SetupContext<E>): any
|
||||
props?: ComponentPropsOptions<P>
|
||||
emits?: E | (keyof E)[]
|
||||
inheritAttrs?: boolean
|
||||
displayName?: string
|
||||
}
|
||||
@ -92,12 +96,29 @@ export const enum LifecycleHooks {
|
||||
ERROR_CAPTURED = 'ec'
|
||||
}
|
||||
|
||||
export type Emit = (event: string, ...args: unknown[]) => any[]
|
||||
type UnionToIntersection<U> = (U extends any
|
||||
? (k: U) => void
|
||||
: never) extends ((k: infer I) => void)
|
||||
? I
|
||||
: never
|
||||
|
||||
export interface SetupContext {
|
||||
export type Emit<
|
||||
Options = Record<string, any>,
|
||||
Event extends keyof Options = keyof Options
|
||||
> = Options extends any[]
|
||||
? (event: Options[0], ...args: any[]) => unknown[]
|
||||
: UnionToIntersection<
|
||||
{
|
||||
[key in Event]: Options[key] extends ((...args: infer Args) => any)
|
||||
? (event: key, ...args: Args) => unknown[]
|
||||
: (event: key, ...args: any[]) => unknown[]
|
||||
}[Event]
|
||||
>
|
||||
|
||||
export interface SetupContext<E = Record<string, any>> {
|
||||
attrs: Data
|
||||
slots: Slots
|
||||
emit: Emit
|
||||
emit: Emit<E>
|
||||
}
|
||||
|
||||
export type RenderFunction = {
|
||||
@ -248,7 +269,7 @@ export function createComponentInstance(
|
||||
rtc: null,
|
||||
ec: null,
|
||||
|
||||
emit: (event, ...args): any[] => {
|
||||
emit: (event: string, ...args: any[]): any[] => {
|
||||
const props = instance.vnode.props || EMPTY_OBJ
|
||||
let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
|
||||
if (!handler && event.indexOf('update:') === 0) {
|
||||
@ -303,9 +324,8 @@ export function setupComponent(
|
||||
isSSR = false
|
||||
) {
|
||||
isInSSRComponentSetup = isSSR
|
||||
const propsOptions = instance.type.props
|
||||
const { props, children, shapeFlag } = instance.vnode
|
||||
resolveProps(instance, props, propsOptions)
|
||||
resolveProps(instance, props)
|
||||
resolveSlots(instance, children)
|
||||
|
||||
// setup stateful logic
|
||||
|
@ -13,10 +13,13 @@ import {
|
||||
PatchFlags,
|
||||
makeMap,
|
||||
isReservedProp,
|
||||
EMPTY_ARR
|
||||
EMPTY_ARR,
|
||||
ShapeFlags,
|
||||
isOn
|
||||
} from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
import { Data, ComponentInternalInstance } from './component'
|
||||
import { EmitsOptions } from './apiOptions'
|
||||
|
||||
export type ComponentPropsOptions<P = Data> =
|
||||
| ComponentObjectPropsOptions<P>
|
||||
@ -103,15 +106,17 @@ type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
|
||||
|
||||
export function resolveProps(
|
||||
instance: ComponentInternalInstance,
|
||||
rawProps: Data | null,
|
||||
_options: ComponentPropsOptions | void
|
||||
rawProps: Data | null
|
||||
) {
|
||||
const _options = instance.type.props
|
||||
const hasDeclaredProps = !!_options
|
||||
if (!rawProps && !hasDeclaredProps) {
|
||||
instance.props = instance.attrs = EMPTY_OBJ
|
||||
return
|
||||
}
|
||||
|
||||
const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
|
||||
const emits = normalizeEmitsOptions(instance.type.emits)
|
||||
const props: Data = {}
|
||||
let attrs: Data | undefined = undefined
|
||||
|
||||
@ -139,20 +144,18 @@ export function resolveProps(
|
||||
}
|
||||
// prop option names are camelized during normalization, so to support
|
||||
// kebab -> camel conversion here we need to camelize the key.
|
||||
if (hasDeclaredProps) {
|
||||
const camelKey = camelize(key)
|
||||
if (hasOwn(options, camelKey)) {
|
||||
setProp(camelKey, value)
|
||||
} else {
|
||||
// Any non-declared props are put into a separate `attrs` object
|
||||
// for spreading. Make sure to preserve original key casing
|
||||
;(attrs || (attrs = {}))[key] = value
|
||||
}
|
||||
} else {
|
||||
setProp(key, value)
|
||||
let camelKey
|
||||
if (hasDeclaredProps && hasOwn(options, (camelKey = camelize(key)))) {
|
||||
setProp(camelKey, value)
|
||||
} else if (!emits || !isListener(emits, key)) {
|
||||
// Any non-declared (either as a prop or an emitted event) props are put
|
||||
// into a separate `attrs` object for spreading. Make sure to preserve
|
||||
// original key casing
|
||||
;(attrs || (attrs = {}))[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDeclaredProps) {
|
||||
// set default values & cast booleans
|
||||
for (let i = 0; i < needCastKeys.length; i++) {
|
||||
@ -186,15 +189,16 @@ export function resolveProps(
|
||||
validateProp(key, props[key], opt, !hasOwn(props, key))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if component has no declared props, $attrs === $props
|
||||
attrs = props
|
||||
}
|
||||
|
||||
// in case of dynamic props, check if we need to delete keys from
|
||||
// the props proxy
|
||||
const { patchFlag } = instance.vnode
|
||||
if (propsProxy && (patchFlag === 0 || patchFlag & PatchFlags.FULL_PROPS)) {
|
||||
if (
|
||||
hasDeclaredProps &&
|
||||
propsProxy &&
|
||||
(patchFlag === 0 || patchFlag & PatchFlags.FULL_PROPS)
|
||||
) {
|
||||
const rawInitialProps = toRaw(propsProxy)
|
||||
for (const key in rawInitialProps) {
|
||||
if (!hasOwn(props, key)) {
|
||||
@ -206,15 +210,18 @@ export function resolveProps(
|
||||
// lock readonly
|
||||
lock()
|
||||
|
||||
instance.props = props
|
||||
if (
|
||||
instance.vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT &&
|
||||
!hasDeclaredProps
|
||||
) {
|
||||
// functional component with optional props: use attrs as props
|
||||
instance.props = attrs || EMPTY_OBJ
|
||||
} else {
|
||||
instance.props = props
|
||||
}
|
||||
instance.attrs = attrs || EMPTY_OBJ
|
||||
}
|
||||
|
||||
const normalizationMap = new WeakMap<
|
||||
ComponentPropsOptions,
|
||||
NormalizedPropsOptions
|
||||
>()
|
||||
|
||||
function validatePropName(key: string) {
|
||||
if (key[0] !== '$') {
|
||||
return true
|
||||
@ -230,10 +237,10 @@ export function normalizePropsOptions(
|
||||
if (!raw) {
|
||||
return EMPTY_ARR as any
|
||||
}
|
||||
if (normalizationMap.has(raw)) {
|
||||
return normalizationMap.get(raw)!
|
||||
if ((raw as any)._n) {
|
||||
return (raw as any)._n
|
||||
}
|
||||
const options: NormalizedPropsOptions[0] = {}
|
||||
const normalized: NormalizedPropsOptions[0] = {}
|
||||
const needCastKeys: NormalizedPropsOptions[1] = []
|
||||
if (isArray(raw)) {
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
@ -242,7 +249,7 @@ export function normalizePropsOptions(
|
||||
}
|
||||
const normalizedKey = camelize(raw[i])
|
||||
if (validatePropName(normalizedKey)) {
|
||||
options[normalizedKey] = EMPTY_OBJ
|
||||
normalized[normalizedKey] = EMPTY_OBJ
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -253,7 +260,7 @@ export function normalizePropsOptions(
|
||||
const normalizedKey = camelize(key)
|
||||
if (validatePropName(normalizedKey)) {
|
||||
const opt = raw[key]
|
||||
const prop: NormalizedProp = (options[normalizedKey] =
|
||||
const prop: NormalizedProp = (normalized[normalizedKey] =
|
||||
isArray(opt) || isFunction(opt) ? { type: opt } : opt)
|
||||
if (prop) {
|
||||
const booleanIndex = getTypeIndex(Boolean, prop.type)
|
||||
@ -269,9 +276,38 @@ export function normalizePropsOptions(
|
||||
}
|
||||
}
|
||||
}
|
||||
const normalized: NormalizedPropsOptions = [options, needCastKeys]
|
||||
normalizationMap.set(raw, normalized)
|
||||
return normalized
|
||||
const normalizedEntry: NormalizedPropsOptions = [normalized, needCastKeys]
|
||||
Object.defineProperty(raw, '_n', { value: normalizedEntry })
|
||||
return normalizedEntry
|
||||
}
|
||||
|
||||
function normalizeEmitsOptions(
|
||||
options: EmitsOptions | undefined
|
||||
): Record<string, any> | undefined {
|
||||
if (!options) {
|
||||
return
|
||||
} else if (isArray(options)) {
|
||||
if ((options as any)._n) {
|
||||
return (options as any)._n
|
||||
}
|
||||
const normalized: Record<string, null> = {}
|
||||
options.forEach(key => (normalized[key] = null))
|
||||
Object.defineProperty(options, '_n', normalized)
|
||||
return normalized
|
||||
} else {
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
function isListener(emits: Record<string, any>, key: string): boolean {
|
||||
if (!isOn(key)) {
|
||||
return false
|
||||
}
|
||||
const eventName = key.slice(2)
|
||||
return (
|
||||
hasOwn(emits, eventName) ||
|
||||
hasOwn(emits, eventName[0].toLowerCase() + eventName.slice(1))
|
||||
)
|
||||
}
|
||||
|
||||
// use function string name to check type constructors
|
||||
|
@ -7,7 +7,8 @@ import {
|
||||
ComponentOptionsBase,
|
||||
ComputedOptions,
|
||||
MethodOptions,
|
||||
resolveMergedOptions
|
||||
resolveMergedOptions,
|
||||
EmitsOptions
|
||||
} from './apiOptions'
|
||||
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
|
||||
import { warn } from './warning'
|
||||
@ -26,6 +27,7 @@ export type ComponentPublicInstance<
|
||||
D = {}, // return from data()
|
||||
C extends ComputedOptions = {},
|
||||
M extends MethodOptions = {},
|
||||
E extends EmitsOptions = {},
|
||||
PublicProps = P
|
||||
> = {
|
||||
$: ComponentInternalInstance
|
||||
@ -36,9 +38,9 @@ export type ComponentPublicInstance<
|
||||
$slots: Slots
|
||||
$root: ComponentInternalInstance | null
|
||||
$parent: ComponentInternalInstance | null
|
||||
$emit: Emit
|
||||
$emit: Emit<E>
|
||||
$el: any
|
||||
$options: ComponentOptionsBase<P, B, D, C, M>
|
||||
$options: ComponentOptionsBase<P, B, D, C, M, E>
|
||||
$forceUpdate: ReactiveEffect
|
||||
$nextTick: typeof nextTick
|
||||
$watch: typeof instanceWatch
|
||||
|
@ -15,7 +15,6 @@ import {
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
createComponentInstance,
|
||||
Component,
|
||||
Data,
|
||||
setupComponent
|
||||
} from './component'
|
||||
@ -1249,7 +1248,7 @@ function baseCreateRenderer(
|
||||
nextVNode.component = instance
|
||||
instance.vnode = nextVNode
|
||||
instance.next = null
|
||||
resolveProps(instance, nextVNode.props, (nextVNode.type as Component).props)
|
||||
resolveProps(instance, nextVNode.props)
|
||||
resolveSlots(instance, nextVNode.children)
|
||||
}
|
||||
|
||||
|
@ -177,35 +177,35 @@ describe('with object props', () => {
|
||||
expectError(<MyComponent b="foo" dd={{ n: 'string' }} ddd={['foo']} />)
|
||||
})
|
||||
|
||||
describe('type inference w/ optional props declaration', () => {
|
||||
const MyComponent = defineComponent({
|
||||
setup(_props: { msg: string }) {
|
||||
return {
|
||||
a: 1
|
||||
}
|
||||
},
|
||||
render() {
|
||||
expectType<string>(this.$props.msg)
|
||||
// props should be readonly
|
||||
expectError((this.$props.msg = 'foo'))
|
||||
// should not expose on `this`
|
||||
expectError(this.msg)
|
||||
expectType<number>(this.a)
|
||||
return null
|
||||
}
|
||||
})
|
||||
// describe('type inference w/ optional props declaration', () => {
|
||||
// const MyComponent = defineComponent({
|
||||
// setup(_props: { msg: string }) {
|
||||
// return {
|
||||
// a: 1
|
||||
// }
|
||||
// },
|
||||
// render() {
|
||||
// expectType<string>(this.$props.msg)
|
||||
// // props should be readonly
|
||||
// expectError((this.$props.msg = 'foo'))
|
||||
// // should not expose on `this`
|
||||
// expectError(this.msg)
|
||||
// expectType<number>(this.a)
|
||||
// return null
|
||||
// }
|
||||
// })
|
||||
|
||||
expectType<JSX.Element>(<MyComponent msg="foo" />)
|
||||
expectError(<MyComponent />)
|
||||
expectError(<MyComponent msg={1} />)
|
||||
})
|
||||
// expectType<JSX.Element>(<MyComponent msg="foo" />)
|
||||
// expectError(<MyComponent />)
|
||||
// expectError(<MyComponent msg={1} />)
|
||||
// })
|
||||
|
||||
describe('type inference w/ direct setup function', () => {
|
||||
const MyComponent = defineComponent((_props: { msg: string }) => {})
|
||||
expectType<JSX.Element>(<MyComponent msg="foo" />)
|
||||
expectError(<MyComponent />)
|
||||
expectError(<MyComponent msg={1} />)
|
||||
})
|
||||
// describe('type inference w/ direct setup function', () => {
|
||||
// const MyComponent = defineComponent((_props: { msg: string }) => {})
|
||||
// expectType<JSX.Element>(<MyComponent msg="foo" />)
|
||||
// expectError(<MyComponent />)
|
||||
// expectError(<MyComponent msg={1} />)
|
||||
// })
|
||||
|
||||
describe('type inference w/ array props declaration', () => {
|
||||
defineComponent({
|
||||
@ -320,3 +320,57 @@ describe('defineComponent', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('emits', () => {
|
||||
// Note: for TSX inference, ideally we want to map emits to onXXX props,
|
||||
// but that requires type-level string constant concatenation as suggested in
|
||||
// https://github.com/Microsoft/TypeScript/issues/12754
|
||||
|
||||
// The workaround for TSX users is instead of using emits, declare onXXX props
|
||||
// and call them instead. Since `v-on:click` compiles to an `onClick` prop,
|
||||
// this would also support other users consuming the component in templates
|
||||
// with `v-on` listeners.
|
||||
|
||||
// with object emits
|
||||
defineComponent({
|
||||
emits: {
|
||||
click: (n: number) => typeof n === 'number',
|
||||
input: (b: string) => null
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
emit('click', 1)
|
||||
emit('input', 'foo')
|
||||
expectError(emit('nope'))
|
||||
expectError(emit('click'))
|
||||
expectError(emit('click', 'foo'))
|
||||
expectError(emit('input'))
|
||||
expectError(emit('input', 1))
|
||||
},
|
||||
created() {
|
||||
this.$emit('click', 1)
|
||||
this.$emit('input', 'foo')
|
||||
expectError(this.$emit('nope'))
|
||||
expectError(this.$emit('click'))
|
||||
expectError(this.$emit('click', 'foo'))
|
||||
expectError(this.$emit('input'))
|
||||
expectError(this.$emit('input', 1))
|
||||
}
|
||||
})
|
||||
|
||||
// with array emits
|
||||
defineComponent({
|
||||
emits: ['foo', 'bar'],
|
||||
setup(props, { emit }) {
|
||||
emit('foo')
|
||||
emit('foo', 123)
|
||||
emit('bar')
|
||||
expectError(emit('nope'))
|
||||
},
|
||||
created() {
|
||||
this.$emit('foo')
|
||||
this.$emit('foo', 123)
|
||||
this.$emit('bar')
|
||||
expectError(this.$emit('nope'))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
39
test-dts/functionalComponent.test-d.tsx
Normal file
39
test-dts/functionalComponent.test-d.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { expectError, expectType } from 'tsd'
|
||||
import { FunctionalComponent } from './index'
|
||||
|
||||
// simple function signature
|
||||
const Foo = (props: { foo: number }) => props.foo
|
||||
|
||||
// TSX
|
||||
expectType<JSX.Element>(<Foo foo={1} />)
|
||||
expectError(<Foo />)
|
||||
expectError(<Foo foo="bar" />)
|
||||
|
||||
// Explicit signature with props + emits
|
||||
const Bar: FunctionalComponent<
|
||||
{ foo: number },
|
||||
{ update: (value: number) => void }
|
||||
> = (props, { emit }) => {
|
||||
expectType<number>(props.foo)
|
||||
|
||||
emit('update', 123)
|
||||
expectError(emit('nope'))
|
||||
expectError(emit('update'))
|
||||
expectError(emit('update', 'nope'))
|
||||
}
|
||||
|
||||
// assigning runtime options
|
||||
Bar.props = {
|
||||
foo: Number
|
||||
}
|
||||
expectError((Bar.props = { foo: String }))
|
||||
|
||||
Bar.emits = {
|
||||
update: value => value > 1
|
||||
}
|
||||
expectError((Bar.emits = { baz: () => void 0 }))
|
||||
|
||||
// TSX
|
||||
expectType<JSX.Element>(<Bar foo={1} />)
|
||||
expectError(<Bar />)
|
||||
expectError(<Bar foo="bar" />)
|
Loading…
Reference in New Issue
Block a user