From d1ae06587b99099ac553215e8330915fb5d1cdb2 Mon Sep 17 00:00:00 2001 From: iwata hidetaka Date: Sat, 19 Sep 2020 07:48:04 +0900 Subject: [PATCH] feat(types): add utility types (#2094) - ComponentProps: Extract props type of component - ComponentListeners: Extract event handlers type of component --- .../runtime-core/src/apiDefineComponent.ts | 4 +- packages/runtime-core/src/componentEmits.ts | 6 + .../src/componentPublicInstance.ts | 47 +++- packages/runtime-core/src/index.ts | 4 +- test-dts/utilityTypes.test-d.tsx | 209 ++++++++++++++++++ 5 files changed, 265 insertions(+), 5 deletions(-) create mode 100644 test-dts/utilityTypes.test-d.tsx diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index 9b85638c2dd..4e034f7e732 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -98,7 +98,7 @@ export function defineComponent< M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, - E extends EmitsOptions = EmitsOptions, + E extends EmitsOptions = Record any>, EE extends string = string >( options: ComponentOptionsWithoutProps< @@ -125,7 +125,7 @@ export function defineComponent< M extends MethodOptions = {}, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, - E extends EmitsOptions = Record, + E extends EmitsOptions = Record any>, EE extends string = string >( options: ComponentOptionsWithArrayProps< diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index 6418f8c7d5d..26e56c36b37 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -25,6 +25,12 @@ export type ObjectEmitsOptions = Record< > export type EmitsOptions = ObjectEmitsOptions | string[] +export type EmitListeners = E extends Array + ? Record void> + : E extends Record void> + ? { [K in keyof E]: (...args: Parameters) => void } + : Record void> + export type EmitFn< Options = ObjectEmitsOptions, Event extends keyof Options = keyof Options diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index e0e239a7997..54f576ada68 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -1,4 +1,9 @@ -import { ComponentInternalInstance, Data } from './component' +import { + ComponentInternalInstance, + Data, + FunctionalComponent, + Component +} from './component' import { nextTick, queueJob } from './scheduler' import { instanceWatch, WatchOptions, WatchStopHandle } from './apiWatch' import { @@ -29,7 +34,7 @@ import { resolveMergedOptions, isInBeforeCreate } from './componentOptions' -import { EmitsOptions, EmitFn } from './componentEmits' +import { EmitsOptions, EmitFn, EmitListeners } from './componentEmits' import { Slots } from './componentSlots' import { currentRenderingInstance, @@ -519,3 +524,41 @@ export function exposeSetupStateOnRenderContext( }) }) } + +type CreateComponentProps< + P, + Defaults, + MixinsType, + PublicP = UnwrapMixinsType & EnsureNonVoid

, + PublicDefaults = UnwrapMixinsType & + EnsureNonVoid +> = Readonly & Omit> + +export type ComponentProps< + C extends Component +> = C extends ComponentOptionsBase< + infer P, + any, + any, + any, + any, + infer Mixin, + infer Extends, + any, + any, + infer Defaults +> + ? CreateComponentProps< + P, + Defaults, + IntersectionMixin & IntersectionMixin + > + : C extends FunctionalComponent ? Readonly

: {} + +export type ComponentListeners< + C extends Component +> = C extends ComponentOptionsBase + ? EmitListeners + : C extends FunctionalComponent + ? EmitListeners + : EmitListeners diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 26c27d544e6..cbda9cdf632 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -182,7 +182,9 @@ export { export { EmitsOptions, ObjectEmitsOptions } from './componentEmits' export { ComponentPublicInstance, - ComponentCustomProperties + ComponentCustomProperties, + ComponentProps, + ComponentListeners } from './componentPublicInstance' export { Renderer, diff --git a/test-dts/utilityTypes.test-d.tsx b/test-dts/utilityTypes.test-d.tsx new file mode 100644 index 00000000000..f6501708d80 --- /dev/null +++ b/test-dts/utilityTypes.test-d.tsx @@ -0,0 +1,209 @@ +import { + FunctionalComponent, + ComponentListeners, + ComponentProps, + defineComponent, + expectType, + expectAssignable +} from './index' + +describe('ComponentListeners', () => { + test('defineComponent', () => { + const C1 = defineComponent({}) + type C1Listeners = ComponentListeners + expectType void>>({} as C1Listeners) + + const C2 = defineComponent({ + emits: ['foo', 'bar'] + }) + type C2Listeners = ComponentListeners + expectType<{ + foo: (...args: any[]) => void + bar: (...args: any[]) => void + }>({} as C2Listeners) + + const C3 = defineComponent({ + emits: { + foo: () => true, + bar: (n: number) => true, + baz: (n: number, s: string) => true + } + }) + type C3Listeners = ComponentListeners + expectType<{ + foo: () => void + bar: (n: number) => void + baz: (n: number, s: string) => void + }>({} as C3Listeners) + }) + + test('functionalComponent', () => { + const C1 = () =>

Hello world
+ type C1Listeners = ComponentListeners + expectType void>>({} as C1Listeners) + + const C2: FunctionalComponent<{}, ['foo', 'bar']> = () => ( +
Hello world
+ ) + type C2Listeners = ComponentListeners + expectType<{ + foo: (...args: any[]) => void + bar: (...args: any[]) => void + }>({} as C2Listeners) + + type ObjectEmits = { + foo: () => void + bar: (n: number) => void + baz: (n: number, s: string) => void + } + const C3: FunctionalComponent<{}, ObjectEmits> = () => ( +
Hello world
+ ) + type C3Listeners = ComponentListeners + expectType({} as C3Listeners) + }) +}) + +describe('ComponentProps', () => { + test('defineComponent', () => { + const C1 = defineComponent({}) + type C1Props = ComponentProps + expectAssignable>({}) + + const C2 = defineComponent({ + props: ['c2p1', 'c2p2', 'c2p3'] + }) + // NOTE: + // When we want to test if properties are required or optional, + // below test does not work because `{ foo: string }` is assignable to `{ foo?: string }` + // + // expectType<{ foo?: string }>({} as { foo: string }) // error expected, but passed + type C2Props = ComponentProps + expectType<{ readonly c2p1: any; readonly c2p2: any; readonly c2p3: any }>( + {} as Required + ) + expectAssignable(undefined) + expectAssignable(undefined) + expectAssignable(undefined) + + const C3 = defineComponent({ + props: { + c3p1: String, + c3p2: { type: Number, required: true }, + c3p3: { type: Boolean, default: true } + } + }) + type C3Props = ComponentProps + expectType<{ + readonly c3p1: string + readonly c3p2: number + readonly c3p3: boolean + }>({} as Required) + expectAssignable(undefined) + expectAssignable(undefined) + // @ts-expect-error + expectAssignable(undefined) + }) + + test('defineComponent - extends and mixins', () => { + const M1 = defineComponent({ + props: ['m1P1'] + }) + const M2 = defineComponent({ + props: { + m2P1: String, + m2P2: { type: Number, required: true }, + m2P3: { type: Number, default: 0 } + } + }) + const B = defineComponent({ + props: { + bP1: Boolean + } + }) + + type CommonPropsExpected = { + m1P1: any + m2P1: string + m2P2: number + m2P3: number + bP1: boolean + } + + const C1 = defineComponent({ + extends: B, + mixins: [M1, M2] + }) + type C1Props = ComponentProps + expectType({} as Required) + expectAssignable(undefined) + expectAssignable(undefined) + expectAssignable(undefined) + expectAssignable(undefined) + // @ts-expect-error + expectAssignable(undefined) + + const C2 = defineComponent({ + extends: B, + mixins: [M1, M2], + props: ['c2P1'] + }) + type C2Props = ComponentProps + expectType< + CommonPropsExpected & { + c2P1: any + } + >({} as Required) + expectAssignable(undefined) + expectAssignable(undefined) + expectAssignable(undefined) + expectAssignable(undefined) + expectAssignable(undefined) + // @ts-expect-error + expectAssignable(undefined) + + const C3 = defineComponent({ + extends: B, + mixins: [M1, M2], + props: { + c3P1: String, + c3P2: { type: Number, required: true }, + c3P3: { type: Number, default: 0 } + } + }) + type C3Props = ComponentProps + expectType< + CommonPropsExpected & { + c3P1: string + c3P2: number + c3P3: number + } + >({} as Required) + expectAssignable(undefined) + expectAssignable(undefined) + expectAssignable(undefined) + expectAssignable(undefined) + expectAssignable(undefined) + expectAssignable(undefined) + // @ts-expect-error + expectAssignable(undefined) + // @ts-expect-error + expectAssignable(undefined) + }) + + test('functionalComponent', () => { + const C1 = () =>
Hello world
+ type C1Props = ComponentProps + expectType({} as keyof C1Props) + + type C2PropsExpected = { name: string; age?: number } + const C2: FunctionalComponent = props => ( +
Hello {props.name}
+ ) + type C2Props = ComponentProps + expectType>({} as Required) + // @ts-expect-error + expectAssignable(undefined) + expectAssignable(undefined) + }) +})