Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(types): enhance the event type in defineEmits #8332

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions packages/dts-test/setupHelpers.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,34 @@ describe('defineProps w/ runtime declaration', () => {
})

describe('defineEmits w/ type declaration', () => {
const emit = defineEmits<(e: 'change') => void>()
const emit =
defineEmits<(e: 'change' | 'foo-bar' | 'update:fooBar') => void>()
emit('change')
emit('foo-bar')
emit('fooBar')
emit('update:fooBar')
emit('update:foo-bar')
// @ts-expect-error
emit()
// @ts-expect-error
emit('bar')

type Emits = { (e: 'foo' | 'bar'): void; (e: 'baz', id: number): void }
type Emits = {
(e: 'foo' | 'bar' | 'foo-bar'): void
(e: 'fooBar', id: string): void
(e: 'update:fooBar', id: string): void
(e: 'baz', id: number): void
}
const emit2 = defineEmits<Emits>()

emit2('foo')
emit2('bar')
emit2('foo-bar')
emit2('fooBar', 'hi')
// @ts-expect-error
emit2('fooBar')
emit2('update:fooBar', 'hi')
emit2('update:foo-bar', 'hi')
emit2('baz', 123)
// @ts-expect-error
emit2('baz')
Expand All @@ -154,6 +170,9 @@ describe('defineEmits w/ alt type declaration', () => {
foo: [id: string]
bar: any[]
baz: []
'foo-bar': []
fooBar: [id: string]
'update:fooBar': []
}>()

emit('foo', 'hi')
Expand All @@ -166,6 +185,14 @@ describe('defineEmits w/ alt type declaration', () => {
emit('baz')
// @ts-expect-error
emit('baz', 1)

emit('foo-bar')
// @ts-expect-error
emit('fooBar')
emit('fooBar', 'hi')

emit('update:fooBar')
emit('update:foo-bar')
})

describe('defineEmits w/ runtime declaration', () => {
Expand Down
31 changes: 26 additions & 5 deletions packages/runtime-core/src/apiSetupHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
isFunction,
Prettify,
UnionToIntersection,
extend
extend,
OverloadUnion
} from '@vue/shared'
import {
getCurrentInstance,
Expand All @@ -13,7 +14,13 @@ import {
createSetupContext,
unsetCurrentInstance
} from './component'
import { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits'
import {
EmitFn,
EmitsOptions,
EnrichEmitEvent,
ExtractEmitEvent,
ObjectEmitsOptions
} from './componentEmits'
import {
ComponentOptionsMixin,
ComponentOptionsWithoutProps,
Expand Down Expand Up @@ -134,7 +141,7 @@ export function defineEmits<E extends EmitsOptions = EmitsOptions>(
): EmitFn<E>
export function defineEmits<
T extends ((...args: any[]) => any) | Record<string, any[]>
>(): T extends (...args: any[]) => any ? T : ShortEmits<T>
>(): T extends (...args: any[]) => any ? ShortEmitFn<T> : ShortEmits<T>
// implementation
export function defineEmits() {
if (__DEV__) {
Expand All @@ -145,9 +152,23 @@ export function defineEmits() {

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

type ShortEmits<T extends Record<string, any>> = UnionToIntersection<
type ShortEmitFn<T extends (...args: any[]) => any> = UnionToIntersection<
OverloadUnion<T> extends infer Fn
? Fn extends (event: infer Event extends string, ...args: infer Args) => any
? (
event: EnrichEmitEvent<Event, ExtractEmitEvent<OverloadUnion<T>>>,
...args: Args
) => void
: T
: T
>

type ShortEmits<
T extends Record<string, any>,
Event extends keyof T = keyof T
> = UnionToIntersection<
RecordToUnion<{
[K in keyof T]: (evt: K, ...args: T[K]) => void
[K in Event]: (evt: EnrichEmitEvent<K, Event>, ...args: T[K]) => void
}>
>

Expand Down
26 changes: 22 additions & 4 deletions packages/runtime-core/src/componentEmits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
isString,
isOn,
UnionToIntersection,
looseToNumber
looseToNumber,
Hyphenate,
Camelize
} from '@vue/shared'
import {
ComponentInternalInstance,
Expand Down Expand Up @@ -55,18 +57,34 @@ export type EmitsToProps<T extends EmitsOptions> = T extends string[]
}
: {}

export type ExtractEmitEvent<T extends (...args: any[]) => any> =
Parameters<T>[0] & string

export type EnrichEmitEvent<T, Event = T> = T extends string
?
| T
| ((
T extends `update:${infer P}` ? `update:${Hyphenate<P>}` : Camelize<T>
) extends infer E
? // preserve the original type of the existing event
E extends Event
? T
: E
: T)
: T

export type EmitFn<
Options = ObjectEmitsOptions,
Event extends keyof Options = keyof Options
> = Options extends Array<infer V>
? (event: V, ...args: any[]) => void
? (event: EnrichEmitEvent<V>, ...args: any[]) => void
: {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function
? (event: string, ...args: any[]) => void
: UnionToIntersection<
{
[key in Event]: Options[key] extends (...args: infer Args) => any
? (event: key, ...args: Args) => void
: (event: key, ...args: any[]) => void
? (event: EnrichEmitEvent<key, Event>, ...args: Args) => void
: (event: EnrichEmitEvent<key, Event>, ...args: any[]) => void
}[Event]
>

Expand Down
74 changes: 74 additions & 0 deletions packages/shared/src/typeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,77 @@ export type LooseRequired<T> = { [P in keyof (T & Required<T>)]: T[P] }
// If the type T accepts type "any", output type Y, otherwise output type N.
// https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360
export type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N

// https://github.com/microsoft/TypeScript/issues/14107#issuecomment-1146738780
type OverloadProps<TOverload> = Pick<TOverload, keyof TOverload>

type OverloadUnionRecursive<
TOverload,
TPartialOverload = unknown
> = TOverload extends (...args: infer TArgs) => infer TReturn
? // Prevent infinite recursion by stopping recursion when TPartialOverload
// has accumulated all of the TOverload signatures.
TPartialOverload extends TOverload
? never
:
| OverloadUnionRecursive<
TPartialOverload & TOverload,
TPartialOverload &
((...args: TArgs) => TReturn) &
OverloadProps<TOverload>
>
| ((...args: TArgs) => TReturn)
: never

export type OverloadUnion<TOverload extends (...args: any[]) => any> = Exclude<
OverloadUnionRecursive<
// The "() => never" signature must be hoisted to the "front" of the
// intersection, for two reasons: a) because recursion stops when it is
// encountered, and b) it seems to prevent the collapse of subsequent
// "compatible" signatures (eg. "() => void" into "(a?: 1) => void"),
// which gives a direct conversion to a union.
(() => never) & TOverload
>,
TOverload extends () => never ? never : () => never
>

export type Camelize<T extends string> =
T extends `${infer FirstPart}-${infer SecondPart}`
? `${FirstPart}${Camelize<Capitalize<SecondPart>>}`
: T

export type Hyphenate<T extends string> = Hyphenate_<Uncapitalize<T>>
type Hyphenate_<T extends string> =
T extends `${infer FirstPart}${infer SecondPart}`
? FirstPart extends UpperCaseCharacters
? `-${Lowercase<FirstPart>}${Hyphenate_<SecondPart>}`
: `${FirstPart}${Hyphenate_<SecondPart>}`
: T

export type UpperCaseCharacters =
| 'A'
| 'B'
| 'C'
| 'D'
| 'E'
| 'F'
| 'G'
| 'H'
| 'I'
| 'J'
| 'K'
| 'L'
| 'M'
| 'N'
| 'O'
| 'P'
| 'Q'
| 'R'
| 'S'
| 'T'
| 'U'
| 'V'
| 'W'
| 'X'
| 'Y'
| 'Z'