Skip to content

Commit

Permalink
feat(types): add __typeEmits for directly inferring emits from user t…
Browse files Browse the repository at this point in the history
…ypes
  • Loading branch information
yyx990803 committed Apr 26, 2024
1 parent e610a86 commit 9d7984e
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 156 deletions.
99 changes: 99 additions & 0 deletions packages/dts-test/defineComponent.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,7 @@ describe('__typeProps backdoor for union type for conditional props', () => {
;<Comp color="white" />
// @ts-expect-error
;<Comp color="white" appearance="normal" />
;<Comp color="white" appearance="outline" />

const c = new Comp()
// @ts-expect-error
Expand All @@ -1656,3 +1657,101 @@ describe('__typeProps backdoor for union type for conditional props', () => {
c.$props = { color: 'white', appearance: 'text' }
c.$props = { color: 'white', appearance: 'outline' }
})

describe('__typeEmits backdoor, 3.3+ object syntax', () => {
type Emits = {
change: [id: number]
update: [value: string]
}

const Comp = defineComponent({
__typeEmits: {} as Emits,
mounted() {
this.$props.onChange?.(123)
// @ts-expect-error
this.$props.onChange?.('123')
this.$props.onUpdate?.('foo')
// @ts-expect-error
this.$props.onUpdate?.(123)

// @ts-expect-error
this.$emit('foo')

this.$emit('change', 123)
// @ts-expect-error
this.$emit('change', '123')

this.$emit('update', 'test')
// @ts-expect-error
this.$emit('update', 123)
},
})

;<Comp onChange={id => id.toFixed(2)} />
;<Comp onUpdate={id => id.toUpperCase()} />
// @ts-expect-error
;<Comp onChange={id => id.slice(1)} />
// @ts-expect-error
;<Comp onUpdate={id => id.toFixed(2)} />

const c = new Comp()
// @ts-expect-error
c.$emit('foo')

c.$emit('change', 123)
// @ts-expect-error
c.$emit('change', '123')

c.$emit('update', 'test')
// @ts-expect-error
c.$emit('update', 123)
})

describe('__typeEmits backdoor, call signature syntax', () => {
type Emits = {
(e: 'change', id: number): void
(e: 'update', value: string): void
}

const Comp = defineComponent({
__typeEmits: {} as Emits,
mounted() {
this.$props.onChange?.(123)
// @ts-expect-error
this.$props.onChange?.('123')
this.$props.onUpdate?.('foo')
// @ts-expect-error
this.$props.onUpdate?.(123)

// @ts-expect-error
this.$emit('foo')

this.$emit('change', 123)
// @ts-expect-error
this.$emit('change', '123')

this.$emit('update', 'test')
// @ts-expect-error
this.$emit('update', 123)
},
})

;<Comp onChange={id => id.toFixed(2)} />
;<Comp onUpdate={id => id.toUpperCase()} />
// @ts-expect-error
;<Comp onChange={id => id.slice(1)} />
// @ts-expect-error
;<Comp onUpdate={id => id.toFixed(2)} />

const c = new Comp()
// @ts-expect-error
c.$emit('foo')

c.$emit('change', 123)
// @ts-expect-error
c.$emit('change', '123')

c.$emit('update', 'test')
// @ts-expect-error
c.$emit('update', 123)
})
87 changes: 19 additions & 68 deletions packages/runtime-core/src/apiDefineComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type {
ComponentOptionsMixin,
ComponentOptionsWithArrayProps,
ComponentOptionsWithObjectProps,
ComponentOptionsWithTypeProps,
ComponentOptionsWithoutProps,
ComponentProvideOptions,
ComputedOptions,
Expand All @@ -26,7 +25,11 @@ import type {
ExtractDefaultPropTypes,
ExtractPropTypes,
} from './componentProps'
import type { EmitsOptions, EmitsToProps } from './componentEmits'
import type {
EmitsOptions,
EmitsToProps,
TypeEmitsToOptions,
} from './componentEmits'
import { extend, isFunction } from '@vue/shared'
import type { VNodeProps } from './vnode'
import type {
Expand All @@ -35,6 +38,7 @@ import type {
} from './componentPublicInstance'
import type { SlotsType } from './componentSlots'
import type { Directive } from './directives'
import type { TypeEmits } from './apiSetupHelpers'

export type PublicProps = VNodeProps &
AllowedComponentProps &
Expand Down Expand Up @@ -171,7 +175,7 @@ export function defineComponent<
},
): DefineSetupFnComponent<Props, E, S>

// overload 2: object format with internal type props
// overload 2: object format no props or internal type props
// backdoor for Vue Language Service
export function defineComponent<
Props = {},
Expand All @@ -190,8 +194,12 @@ export function defineComponent<
Directives extends Record<string, Directive> = {},
Exposed extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
TE extends TypeEmits = {},
ResolvedEmits extends EmitsOptions = {} extends E
? TypeEmitsToOptions<TE>
: E,
>(
options: ComponentOptionsWithTypeProps<
options: ComponentOptionsWithoutProps<
Props,
RawBindings,
D,
Expand All @@ -207,7 +215,9 @@ export function defineComponent<
LC,
Directives,
Exposed,
Provide
Provide,
TE,
ResolvedEmits
>,
): DefineComponent<
Props,
Expand All @@ -217,10 +227,10 @@ export function defineComponent<
M,
Mixin,
Extends,
E,
ResolvedEmits,
EE,
PublicProps,
ResolveProps<Props, E>,
ResolveProps<Props, ResolvedEmits>,
ExtractDefaultPropTypes<Props>,
S,
LC,
Expand All @@ -230,66 +240,7 @@ export function defineComponent<
false
>

// overload 3: object format with no props
// (uses user defined props interface)
// return type is for language-tools and TSX support
export function defineComponent<
Props = {},
RawBindings = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {},
EE extends string = string,
I extends ComponentInjectOptions = {},
II extends string = string,
S extends SlotsType = {},
LC extends Record<string, Component> = {},
Directives extends Record<string, Directive> = {},
Exposed extends string = string,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
>(
options: ComponentOptionsWithoutProps<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE,
I,
II,
S,
LC,
Directives,
Exposed,
Provide
>,
): DefineComponent<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
EE,
PublicProps,
ResolveProps<Props, E>,
ExtractDefaultPropTypes<Props>,
S,
LC,
Directives,
Exposed,
Provide
>

// overload 4: object format with array props declaration
// overload 3: object format with array props declaration
// props inferred as { [key in PropNames]?: any }
// return type is for language-tools and TSX support
export function defineComponent<
Expand Down Expand Up @@ -349,7 +300,7 @@ export function defineComponent<
Provide
>

// overload 5: object format with object props declaration
// overload 4: object format with object props declaration
// see `ExtractPropTypes` in ./componentProps.ts
export function defineComponent<
// the Readonly constraint allows TS to treat the type of { required: true }
Expand Down
36 changes: 29 additions & 7 deletions packages/runtime-core/src/apiSetupHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {
} from './component'
import type { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits'
import type {
ComponentOptionsBase,
ComponentOptionsMixin,
ComponentOptionsWithoutProps,
ComputedOptions,
MethodOptions,
} from './componentOptions'
Expand Down Expand Up @@ -135,9 +135,11 @@ export function defineEmits<EE extends string = string>(
export function defineEmits<E extends EmitsOptions = EmitsOptions>(
emitOptions: E,
): EmitFn<E>
export function defineEmits<
T extends ((...args: any[]) => any) | Record<string, any[]>,
>(): T extends (...args: any[]) => any ? T : ShortEmits<T>
export function defineEmits<T extends TypeEmits>(): T extends (
...args: any[]
) => any
? T
: ShortEmits<T>
// implementation
export function defineEmits() {
if (__DEV__) {
Expand All @@ -146,6 +148,8 @@ export function defineEmits() {
return null as any
}

export type TypeEmits = ((...args: any[]) => any) | Record<string, any[]>

type RecordToUnion<T extends Record<string, any>> = T[keyof T]

type ShortEmits<T extends Record<string, any>> = UnionToIntersection<
Expand Down Expand Up @@ -191,15 +195,33 @@ export function defineOptions<
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
>(
options?: ComponentOptionsWithoutProps<
options?: ComponentOptionsBase<
{},
RawBindings,
D,
C,
M,
Mixin,
Extends
> & { emits?: undefined; expose?: undefined; slots?: undefined },
Extends,
{}
> & {
/**
* props should be defined via defineProps().
*/
props: never
/**
* emits should be defined via defineEmits().
*/
emits?: never
/**
* expose should be defined via defineExpose().
*/
expose?: never
/**
* slots should be defined via defineSlots().
*/
slots?: never
},
): void {
if (__DEV__) {
warnRuntimeUsage(`defineOptions`)
Expand Down

0 comments on commit 9d7984e

Please sign in to comment.