feat(runtime-core): type and attr fallthrough support for emits option

This commit is contained in:
Evan You 2020-04-03 12:05:52 -04:00
parent c409d4f297
commit bf473a64ea
9 changed files with 350 additions and 96 deletions

View File

@ -8,7 +8,8 @@ import {
onUpdated, onUpdated,
defineComponent, defineComponent,
openBlock, openBlock,
createBlock createBlock,
FunctionalComponent
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import { mockWarn } from '@vue/shared' import { mockWarn } from '@vue/shared'
@ -428,4 +429,70 @@ describe('attribute fallthrough', () => {
await nextTick() await nextTick()
expect(root.innerHTML).toBe(`<div aria-hidden="false" class="barr"></div>`) 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')
})
}) })

View File

@ -3,7 +3,8 @@ import {
MethodOptions, MethodOptions,
ComponentOptionsWithoutProps, ComponentOptionsWithoutProps,
ComponentOptionsWithArrayProps, ComponentOptionsWithArrayProps,
ComponentOptionsWithObjectProps ComponentOptionsWithObjectProps,
EmitsOptions
} from './apiOptions' } from './apiOptions'
import { SetupContext, RenderFunction } from './component' import { SetupContext, RenderFunction } from './component'
import { ComponentPublicInstance } from './componentProxy' import { ComponentPublicInstance } from './componentProxy'
@ -39,13 +40,15 @@ export function defineComponent<Props, RawBindings = object>(
// (uses user defined props interface) // (uses user defined props interface)
// return type is for Vetur and TSX support // return type is for Vetur and TSX support
export function defineComponent< export function defineComponent<
Props, Props = {},
RawBindings, RawBindings = {},
D, D = {},
C extends ComputedOptions = {}, 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< new (): ComponentPublicInstance<
Props, Props,
@ -53,6 +56,7 @@ export function defineComponent<
D, D,
C, C,
M, M,
E,
VNodeProps & Props VNodeProps & Props
> >
} }
@ -65,12 +69,22 @@ export function defineComponent<
RawBindings, RawBindings,
D, D,
C extends ComputedOptions = {}, 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 // 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 // overload 4: object format with object props declaration
@ -82,9 +96,19 @@ export function defineComponent<
RawBindings, RawBindings,
D, D,
C extends ComputedOptions = {}, 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< new (): ComponentPublicInstance<
ExtractPropTypes<PropsOptions>, ExtractPropTypes<PropsOptions>,
@ -92,6 +116,7 @@ export function defineComponent<
D, D,
C, C,
M, M,
E,
VNodeProps & ExtractPropTypes<PropsOptions, false> VNodeProps & ExtractPropTypes<PropsOptions, false>
> >
} }

View File

@ -50,12 +50,14 @@ export interface ComponentOptionsBase<
RawBindings, RawBindings,
D, D,
C extends ComputedOptions, C extends ComputedOptions,
M extends MethodOptions M extends MethodOptions,
> extends LegacyOptions<Props, RawBindings, D, C, M>, SFCInternalOptions { E extends EmitsOptions,
EE extends string = string
> extends LegacyOptions<Props, D, C, M>, SFCInternalOptions {
setup?: ( setup?: (
this: void, this: void,
props: Props, props: Props,
ctx: SetupContext ctx: SetupContext<E>
) => RawBindings | RenderFunction | void ) => RawBindings | RenderFunction | void
name?: string name?: string
template?: string | object // can be a direct DOM node template?: string | object // can be a direct DOM node
@ -75,6 +77,7 @@ export interface ComponentOptionsBase<
components?: Record<string, PublicAPIComponent> components?: Record<string, PublicAPIComponent>
directives?: Record<string, Directive> directives?: Record<string, Directive>
inheritAttrs?: boolean inheritAttrs?: boolean
emits?: E | EE[]
// Internal ------------------------------------------------------------------ // Internal ------------------------------------------------------------------
@ -97,10 +100,14 @@ export type ComponentOptionsWithoutProps<
RawBindings = {}, RawBindings = {},
D = {}, D = {},
C extends ComputedOptions = {}, C extends ComputedOptions = {},
M extends MethodOptions = {} M extends MethodOptions = {},
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & { E extends EmitsOptions = Record<string, any>,
EE extends string = string
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
props?: undefined props?: undefined
} & ThisType<ComponentPublicInstance<{}, RawBindings, D, C, M, Readonly<Props>>> } & ThisType<
ComponentPublicInstance<{}, RawBindings, D, C, M, E, Readonly<Props>>
>
export type ComponentOptionsWithArrayProps< export type ComponentOptionsWithArrayProps<
PropNames extends string = string, PropNames extends string = string,
@ -108,10 +115,12 @@ export type ComponentOptionsWithArrayProps<
D = {}, D = {},
C extends ComputedOptions = {}, C extends ComputedOptions = {},
M extends MethodOptions = {}, M extends MethodOptions = {},
E extends EmitsOptions = Record<string, any>,
EE extends string = string,
Props = Readonly<{ [key in PropNames]?: any }> Props = Readonly<{ [key in PropNames]?: any }>
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & { > = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
props: PropNames[] props: PropNames[]
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>> } & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
export type ComponentOptionsWithObjectProps< export type ComponentOptionsWithObjectProps<
PropsOptions = ComponentObjectPropsOptions, PropsOptions = ComponentObjectPropsOptions,
@ -119,10 +128,12 @@ export type ComponentOptionsWithObjectProps<
D = {}, D = {},
C extends ComputedOptions = {}, C extends ComputedOptions = {},
M extends MethodOptions = {}, M extends MethodOptions = {},
E extends EmitsOptions = Record<string, any>,
EE extends string = string,
Props = Readonly<ExtractPropTypes<PropsOptions>> Props = Readonly<ExtractPropTypes<PropsOptions>>
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & { > = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
props: PropsOptions props: PropsOptions
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>> } & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
export type ComponentOptions = export type ComponentOptions =
| ComponentOptionsWithoutProps<any, any, any, any, any> | ComponentOptionsWithoutProps<any, any, any, any, any>
@ -138,6 +149,8 @@ export interface MethodOptions {
[key: string]: Function [key: string]: Function
} }
export type EmitsOptions = Record<string, any> | string[]
export type ExtractComputedReturns<T extends any> = { export type ExtractComputedReturns<T extends any> = {
[key in keyof T]: T[key] extends { get: Function } [key in keyof T]: T[key] extends { get: Function }
? ReturnType<T[key]['get']> ? ReturnType<T[key]['get']>
@ -162,7 +175,6 @@ type ComponentInjectOptions =
export interface LegacyOptions< export interface LegacyOptions<
Props, Props,
RawBindings,
D, D,
C extends ComputedOptions, C extends ComputedOptions,
M extends MethodOptions M extends MethodOptions

View File

@ -21,7 +21,7 @@ import {
} from './errorHandling' } from './errorHandling'
import { AppContext, createAppContext, AppConfig } from './apiCreateApp' import { AppContext, createAppContext, AppConfig } from './apiCreateApp'
import { Directive, validateDirectiveName } from './directives' import { Directive, validateDirectiveName } from './directives'
import { applyOptions, ComponentOptions } from './apiOptions' import { applyOptions, ComponentOptions, EmitsOptions } from './apiOptions'
import { import {
EMPTY_OBJ, EMPTY_OBJ,
isFunction, isFunction,
@ -52,9 +52,13 @@ export interface SFCInternalOptions {
__hmrUpdated?: boolean __hmrUpdated?: boolean
} }
export interface FunctionalComponent<P = {}> extends SFCInternalOptions { export interface FunctionalComponent<
(props: P, ctx: SetupContext): VNodeChild P = {},
E extends EmitsOptions = Record<string, any>
> extends SFCInternalOptions {
(props: P, ctx: SetupContext<E>): any
props?: ComponentPropsOptions<P> props?: ComponentPropsOptions<P>
emits?: E | (keyof E)[]
inheritAttrs?: boolean inheritAttrs?: boolean
displayName?: string displayName?: string
} }
@ -92,12 +96,29 @@ export const enum LifecycleHooks {
ERROR_CAPTURED = 'ec' 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 attrs: Data
slots: Slots slots: Slots
emit: Emit emit: Emit<E>
} }
export type RenderFunction = { export type RenderFunction = {
@ -248,7 +269,7 @@ export function createComponentInstance(
rtc: null, rtc: null,
ec: null, ec: null,
emit: (event, ...args): any[] => { emit: (event: string, ...args: any[]): any[] => {
const props = instance.vnode.props || EMPTY_OBJ const props = instance.vnode.props || EMPTY_OBJ
let handler = props[`on${event}`] || props[`on${capitalize(event)}`] let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
if (!handler && event.indexOf('update:') === 0) { if (!handler && event.indexOf('update:') === 0) {
@ -303,9 +324,8 @@ export function setupComponent(
isSSR = false isSSR = false
) { ) {
isInSSRComponentSetup = isSSR isInSSRComponentSetup = isSSR
const propsOptions = instance.type.props
const { props, children, shapeFlag } = instance.vnode const { props, children, shapeFlag } = instance.vnode
resolveProps(instance, props, propsOptions) resolveProps(instance, props)
resolveSlots(instance, children) resolveSlots(instance, children)
// setup stateful logic // setup stateful logic

View File

@ -13,10 +13,13 @@ import {
PatchFlags, PatchFlags,
makeMap, makeMap,
isReservedProp, isReservedProp,
EMPTY_ARR EMPTY_ARR,
ShapeFlags,
isOn
} from '@vue/shared' } from '@vue/shared'
import { warn } from './warning' import { warn } from './warning'
import { Data, ComponentInternalInstance } from './component' import { Data, ComponentInternalInstance } from './component'
import { EmitsOptions } from './apiOptions'
export type ComponentPropsOptions<P = Data> = export type ComponentPropsOptions<P = Data> =
| ComponentObjectPropsOptions<P> | ComponentObjectPropsOptions<P>
@ -103,15 +106,17 @@ type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
export function resolveProps( export function resolveProps(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
rawProps: Data | null, rawProps: Data | null
_options: ComponentPropsOptions | void
) { ) {
const _options = instance.type.props
const hasDeclaredProps = !!_options const hasDeclaredProps = !!_options
if (!rawProps && !hasDeclaredProps) { if (!rawProps && !hasDeclaredProps) {
instance.props = instance.attrs = EMPTY_OBJ
return return
} }
const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)! const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
const emits = normalizeEmitsOptions(instance.type.emits)
const props: Data = {} const props: Data = {}
let attrs: Data | undefined = undefined let attrs: Data | undefined = undefined
@ -139,20 +144,18 @@ export function resolveProps(
} }
// prop option names are camelized during normalization, so to support // prop option names are camelized during normalization, so to support
// kebab -> camel conversion here we need to camelize the key. // kebab -> camel conversion here we need to camelize the key.
if (hasDeclaredProps) { let camelKey
const camelKey = camelize(key) if (hasDeclaredProps && hasOwn(options, (camelKey = camelize(key)))) {
if (hasOwn(options, camelKey)) {
setProp(camelKey, value) setProp(camelKey, value)
} else { } else if (!emits || !isListener(emits, key)) {
// Any non-declared props are put into a separate `attrs` object // Any non-declared (either as a prop or an emitted event) props are put
// for spreading. Make sure to preserve original key casing // into a separate `attrs` object for spreading. Make sure to preserve
// original key casing
;(attrs || (attrs = {}))[key] = value ;(attrs || (attrs = {}))[key] = value
} }
} else {
setProp(key, value)
}
} }
} }
if (hasDeclaredProps) { if (hasDeclaredProps) {
// set default values & cast booleans // set default values & cast booleans
for (let i = 0; i < needCastKeys.length; i++) { for (let i = 0; i < needCastKeys.length; i++) {
@ -186,15 +189,16 @@ export function resolveProps(
validateProp(key, props[key], opt, !hasOwn(props, key)) 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 // in case of dynamic props, check if we need to delete keys from
// the props proxy // the props proxy
const { patchFlag } = instance.vnode 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) const rawInitialProps = toRaw(propsProxy)
for (const key in rawInitialProps) { for (const key in rawInitialProps) {
if (!hasOwn(props, key)) { if (!hasOwn(props, key)) {
@ -206,15 +210,18 @@ export function resolveProps(
// lock readonly // lock readonly
lock() lock()
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.props = props
}
instance.attrs = attrs || EMPTY_OBJ instance.attrs = attrs || EMPTY_OBJ
} }
const normalizationMap = new WeakMap<
ComponentPropsOptions,
NormalizedPropsOptions
>()
function validatePropName(key: string) { function validatePropName(key: string) {
if (key[0] !== '$') { if (key[0] !== '$') {
return true return true
@ -230,10 +237,10 @@ export function normalizePropsOptions(
if (!raw) { if (!raw) {
return EMPTY_ARR as any return EMPTY_ARR as any
} }
if (normalizationMap.has(raw)) { if ((raw as any)._n) {
return normalizationMap.get(raw)! return (raw as any)._n
} }
const options: NormalizedPropsOptions[0] = {} const normalized: NormalizedPropsOptions[0] = {}
const needCastKeys: NormalizedPropsOptions[1] = [] const needCastKeys: NormalizedPropsOptions[1] = []
if (isArray(raw)) { if (isArray(raw)) {
for (let i = 0; i < raw.length; i++) { for (let i = 0; i < raw.length; i++) {
@ -242,7 +249,7 @@ export function normalizePropsOptions(
} }
const normalizedKey = camelize(raw[i]) const normalizedKey = camelize(raw[i])
if (validatePropName(normalizedKey)) { if (validatePropName(normalizedKey)) {
options[normalizedKey] = EMPTY_OBJ normalized[normalizedKey] = EMPTY_OBJ
} }
} }
} else { } else {
@ -253,7 +260,7 @@ export function normalizePropsOptions(
const normalizedKey = camelize(key) const normalizedKey = camelize(key)
if (validatePropName(normalizedKey)) { if (validatePropName(normalizedKey)) {
const opt = raw[key] const opt = raw[key]
const prop: NormalizedProp = (options[normalizedKey] = const prop: NormalizedProp = (normalized[normalizedKey] =
isArray(opt) || isFunction(opt) ? { type: opt } : opt) isArray(opt) || isFunction(opt) ? { type: opt } : opt)
if (prop) { if (prop) {
const booleanIndex = getTypeIndex(Boolean, prop.type) const booleanIndex = getTypeIndex(Boolean, prop.type)
@ -269,9 +276,38 @@ export function normalizePropsOptions(
} }
} }
} }
const normalized: NormalizedPropsOptions = [options, needCastKeys] const normalizedEntry: NormalizedPropsOptions = [normalized, needCastKeys]
normalizationMap.set(raw, normalized) 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 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 // use function string name to check type constructors

View File

@ -7,7 +7,8 @@ import {
ComponentOptionsBase, ComponentOptionsBase,
ComputedOptions, ComputedOptions,
MethodOptions, MethodOptions,
resolveMergedOptions resolveMergedOptions,
EmitsOptions
} from './apiOptions' } from './apiOptions'
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity' import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
import { warn } from './warning' import { warn } from './warning'
@ -26,6 +27,7 @@ export type ComponentPublicInstance<
D = {}, // return from data() D = {}, // return from data()
C extends ComputedOptions = {}, C extends ComputedOptions = {},
M extends MethodOptions = {}, M extends MethodOptions = {},
E extends EmitsOptions = {},
PublicProps = P PublicProps = P
> = { > = {
$: ComponentInternalInstance $: ComponentInternalInstance
@ -36,9 +38,9 @@ export type ComponentPublicInstance<
$slots: Slots $slots: Slots
$root: ComponentInternalInstance | null $root: ComponentInternalInstance | null
$parent: ComponentInternalInstance | null $parent: ComponentInternalInstance | null
$emit: Emit $emit: Emit<E>
$el: any $el: any
$options: ComponentOptionsBase<P, B, D, C, M> $options: ComponentOptionsBase<P, B, D, C, M, E>
$forceUpdate: ReactiveEffect $forceUpdate: ReactiveEffect
$nextTick: typeof nextTick $nextTick: typeof nextTick
$watch: typeof instanceWatch $watch: typeof instanceWatch

View File

@ -15,7 +15,6 @@ import {
import { import {
ComponentInternalInstance, ComponentInternalInstance,
createComponentInstance, createComponentInstance,
Component,
Data, Data,
setupComponent setupComponent
} from './component' } from './component'
@ -1249,7 +1248,7 @@ function baseCreateRenderer(
nextVNode.component = instance nextVNode.component = instance
instance.vnode = nextVNode instance.vnode = nextVNode
instance.next = null instance.next = null
resolveProps(instance, nextVNode.props, (nextVNode.type as Component).props) resolveProps(instance, nextVNode.props)
resolveSlots(instance, nextVNode.children) resolveSlots(instance, nextVNode.children)
} }

View File

@ -177,35 +177,35 @@ describe('with object props', () => {
expectError(<MyComponent b="foo" dd={{ n: 'string' }} ddd={['foo']} />) expectError(<MyComponent b="foo" dd={{ n: 'string' }} ddd={['foo']} />)
}) })
describe('type inference w/ optional props declaration', () => { // describe('type inference w/ optional props declaration', () => {
const MyComponent = defineComponent({ // const MyComponent = defineComponent({
setup(_props: { msg: string }) { // setup(_props: { msg: string }) {
return { // return {
a: 1 // a: 1
} // }
}, // },
render() { // render() {
expectType<string>(this.$props.msg) // expectType<string>(this.$props.msg)
// props should be readonly // // props should be readonly
expectError((this.$props.msg = 'foo')) // expectError((this.$props.msg = 'foo'))
// should not expose on `this` // // should not expose on `this`
expectError(this.msg) // expectError(this.msg)
expectType<number>(this.a) // expectType<number>(this.a)
return null // return null
} // }
}) // })
expectType<JSX.Element>(<MyComponent msg="foo" />) // expectType<JSX.Element>(<MyComponent msg="foo" />)
expectError(<MyComponent />) // expectError(<MyComponent />)
expectError(<MyComponent msg={1} />) // expectError(<MyComponent msg={1} />)
}) // })
describe('type inference w/ direct setup function', () => { // describe('type inference w/ direct setup function', () => {
const MyComponent = defineComponent((_props: { msg: string }) => {}) // const MyComponent = defineComponent((_props: { msg: string }) => {})
expectType<JSX.Element>(<MyComponent msg="foo" />) // expectType<JSX.Element>(<MyComponent msg="foo" />)
expectError(<MyComponent />) // expectError(<MyComponent />)
expectError(<MyComponent msg={1} />) // expectError(<MyComponent msg={1} />)
}) // })
describe('type inference w/ array props declaration', () => { describe('type inference w/ array props declaration', () => {
defineComponent({ 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'))
}
})
})

View 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" />)