From 0eb713c6b8227010f12bc750c140c3d63f22f98b Mon Sep 17 00:00:00 2001 From: nooooooom Date: Tue, 16 May 2023 16:51:07 +0800 Subject: [PATCH 1/5] feat(types): enhance the event type in defineEmits --- packages/runtime-core/src/componentEmits.ts | 23 ++++++++++-- packages/shared/src/typeUtils.ts | 41 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index 7568741e24e..7e94969ddf0 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -11,7 +11,9 @@ import { isString, isOn, UnionToIntersection, - looseToNumber + looseToNumber, + Hyphenate, + Camelize } from '@vue/shared' import { ComponentInternalInstance, @@ -55,18 +57,31 @@ export type EmitsToProps = T extends string[] } : {} +type EnhanceEmitEvent = T extends string + ? + | T + | (( + T extends `update:${infer P}` ? `update:${Hyphenate

}` : Camelize + ) 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 - ? (event: V, ...args: any[]) => void + ? (event: EnhanceEmitEvent, ...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: EnhanceEmitEvent, ...args: Args) => void + : (event: EnhanceEmitEvent, ...args: any[]) => void }[Event] > diff --git a/packages/shared/src/typeUtils.ts b/packages/shared/src/typeUtils.ts index 67fb47c23b3..cf32d9a8013 100644 --- a/packages/shared/src/typeUtils.ts +++ b/packages/shared/src/typeUtils.ts @@ -12,3 +12,44 @@ export type LooseRequired = { [P in keyof (T & Required)]: 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 = 0 extends 1 & T ? Y : N + +export type Camelize = + T extends `${infer FirstPart}-${infer SecondPart}` + ? `${FirstPart}${Camelize>}` + : T + +export type Hyphenate = Hyphenate_> +type Hyphenate_ = + T extends `${infer FirstPart}${infer SecondPart}` + ? FirstPart extends UpperCaseCharacters + ? `-${Lowercase}${Hyphenate_}` + : `${FirstPart}${Hyphenate_}` + : 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' From ca78ea600ed71f1db75f1fc83225e0a5ee1fc30c Mon Sep 17 00:00:00 2001 From: nooooooom Date: Wed, 17 May 2023 10:04:49 +0800 Subject: [PATCH 2/5] chore: rename to EnrichEmitEvent & support short emits --- packages/runtime-core/src/apiSetupHelpers.ts | 14 +++++++++++--- packages/runtime-core/src/componentEmits.ts | 8 ++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts index 3ad330a1c27..8f06aef5f10 100644 --- a/packages/runtime-core/src/apiSetupHelpers.ts +++ b/packages/runtime-core/src/apiSetupHelpers.ts @@ -13,7 +13,12 @@ import { createSetupContext, unsetCurrentInstance } from './component' -import { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits' +import { + EmitFn, + EmitsOptions, + EnrichEmitEvent, + ObjectEmitsOptions +} from './componentEmits' import { ComponentOptionsMixin, ComponentOptionsWithoutProps, @@ -145,9 +150,12 @@ export function defineEmits() { type RecordToUnion> = T[keyof T] -type ShortEmits> = UnionToIntersection< +type ShortEmits< + T extends Record, + Event extends keyof T = keyof T +> = UnionToIntersection< RecordToUnion<{ - [K in keyof T]: (evt: K, ...args: T[K]) => void + [K in Event]: (evt: EnrichEmitEvent, ...args: T[K]) => void }> > diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index 7e94969ddf0..e68e9a29dfc 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -57,7 +57,7 @@ export type EmitsToProps = T extends string[] } : {} -type EnhanceEmitEvent = T extends string +export type EnrichEmitEvent = T extends string ? | T | (( @@ -74,14 +74,14 @@ export type EmitFn< Options = ObjectEmitsOptions, Event extends keyof Options = keyof Options > = Options extends Array - ? (event: EnhanceEmitEvent, ...args: any[]) => void + ? (event: EnrichEmitEvent, ...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: EnhanceEmitEvent, ...args: Args) => void - : (event: EnhanceEmitEvent, ...args: any[]) => void + ? (event: EnrichEmitEvent, ...args: Args) => void + : (event: EnrichEmitEvent, ...args: any[]) => void }[Event] > From 648331adeb929ed279fefa1acfacfaa82f872cc8 Mon Sep 17 00:00:00 2001 From: nooooooom Date: Wed, 17 May 2023 10:08:19 +0800 Subject: [PATCH 3/5] chore: add test --- packages/dts-test/setupHelpers.test-d.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/dts-test/setupHelpers.test-d.ts b/packages/dts-test/setupHelpers.test-d.ts index 9b68b345268..2de8c955776 100644 --- a/packages/dts-test/setupHelpers.test-d.ts +++ b/packages/dts-test/setupHelpers.test-d.ts @@ -154,6 +154,9 @@ describe('defineEmits w/ alt type declaration', () => { foo: [id: string] bar: any[] baz: [] + 'foo-bar': [] + fooBar: [id: string] + 'update:fooBar': [] }>() emit('foo', 'hi') @@ -166,6 +169,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', () => { From 06ce2030633bedb3a217ba4b06240af231834c16 Mon Sep 17 00:00:00 2001 From: nooooooom Date: Wed, 17 May 2023 17:46:19 +0800 Subject: [PATCH 4/5] feat: support overloaded function --- packages/runtime-core/src/apiSetupHelpers.ts | 17 ++++++++-- packages/runtime-core/src/componentEmits.ts | 3 ++ packages/shared/src/typeUtils.ts | 33 ++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts index 8f06aef5f10..9a6880ce434 100644 --- a/packages/runtime-core/src/apiSetupHelpers.ts +++ b/packages/runtime-core/src/apiSetupHelpers.ts @@ -4,7 +4,8 @@ import { isFunction, Prettify, UnionToIntersection, - extend + extend, + OverloadUnion } from '@vue/shared' import { getCurrentInstance, @@ -17,6 +18,7 @@ import { EmitFn, EmitsOptions, EnrichEmitEvent, + ExtractEmitEvent, ObjectEmitsOptions } from './componentEmits' import { @@ -139,7 +141,7 @@ export function defineEmits( ): EmitFn export function defineEmits< T extends ((...args: any[]) => any) | Record ->(): T extends (...args: any[]) => any ? T : ShortEmits +>(): T extends (...args: any[]) => any ? ShortEmitFn : ShortEmits // implementation export function defineEmits() { if (__DEV__) { @@ -150,6 +152,17 @@ export function defineEmits() { type RecordToUnion> = T[keyof T] +type ShortEmitFn any> = UnionToIntersection< + OverloadUnion extends infer Fn + ? Fn extends (event: infer Event extends string, ...args: infer Args) => any + ? ( + event: EnrichEmitEvent>>, + ...args: Args + ) => void + : T + : T +> + type ShortEmits< T extends Record, Event extends keyof T = keyof T diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index e68e9a29dfc..8d2d6c8c604 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -57,6 +57,9 @@ export type EmitsToProps = T extends string[] } : {} +export type ExtractEmitEvent any> = + Parameters[0] & string + export type EnrichEmitEvent = T extends string ? | T diff --git a/packages/shared/src/typeUtils.ts b/packages/shared/src/typeUtils.ts index cf32d9a8013..79f7fd2c726 100644 --- a/packages/shared/src/typeUtils.ts +++ b/packages/shared/src/typeUtils.ts @@ -13,6 +13,39 @@ export type LooseRequired = { [P in keyof (T & Required)]: T[P] } // https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360 export type IfAny = 0 extends 1 & T ? Y : N +// https://github.com/microsoft/TypeScript/issues/14107#issuecomment-1146738780 +type OverloadProps = Pick + +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 + > + | ((...args: TArgs) => TReturn) + : never + +export type OverloadUnion 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 `${infer FirstPart}-${infer SecondPart}` ? `${FirstPart}${Camelize>}` From a99831751fd564165edc925472eff8f585d9ef78 Mon Sep 17 00:00:00 2001 From: nooooooom Date: Wed, 17 May 2023 17:46:49 +0800 Subject: [PATCH 5/5] chore: add test --- packages/dts-test/setupHelpers.test-d.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/dts-test/setupHelpers.test-d.ts b/packages/dts-test/setupHelpers.test-d.ts index 2de8c955776..9e24b53e0cf 100644 --- a/packages/dts-test/setupHelpers.test-d.ts +++ b/packages/dts-test/setupHelpers.test-d.ts @@ -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() 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')