Skip to content

Commit

Permalink
feat(types): add utility types (vuejs#2094)
Browse files Browse the repository at this point in the history
- ComponentProps: Extract props type of component
- ComponentListeners: Extract event handlers type of component
  • Loading branch information
wonderful-panda committed Sep 21, 2020
1 parent d8c1536 commit d1ae065
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 5 deletions.
4 changes: 2 additions & 2 deletions packages/runtime-core/src/apiDefineComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, (...args: any[]) => any>,
EE extends string = string
>(
options: ComponentOptionsWithoutProps<
Expand All @@ -125,7 +125,7 @@ export function defineComponent<
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = Record<string, any>,
E extends EmitsOptions = Record<string, (...args: any[]) => any>,
EE extends string = string
>(
options: ComponentOptionsWithArrayProps<
Expand Down
6 changes: 6 additions & 0 deletions packages/runtime-core/src/componentEmits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export type ObjectEmitsOptions = Record<
>
export type EmitsOptions = ObjectEmitsOptions | string[]

export type EmitListeners<E extends EmitsOptions> = E extends Array<infer V>
? Record<V & string, (...args: any[]) => void>
: E extends Record<string, (...args: any[]) => void>
? { [K in keyof E]: (...args: Parameters<E[K]>) => void }
: Record<string, (...args: any[]) => void>

export type EmitFn<
Options = ObjectEmitsOptions,
Event extends keyof Options = keyof Options
Expand Down
47 changes: 45 additions & 2 deletions packages/runtime-core/src/componentPublicInstance.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -519,3 +524,41 @@ export function exposeSetupStateOnRenderContext(
})
})
}

type CreateComponentProps<
P,
Defaults,
MixinsType,
PublicP = UnwrapMixinsType<MixinsType, 'P'> & EnsureNonVoid<P>,
PublicDefaults = UnwrapMixinsType<MixinsType, 'Defaults'> &
EnsureNonVoid<Defaults>
> = Readonly<Partial<PublicDefaults> & Omit<PublicP, keyof PublicDefaults>>

export type ComponentProps<
C extends Component<any, any>
> = C extends ComponentOptionsBase<
infer P,
any,
any,
any,
any,
infer Mixin,
infer Extends,
any,
any,
infer Defaults
>
? CreateComponentProps<
P,
Defaults,
IntersectionMixin<Mixin> & IntersectionMixin<Extends>
>
: C extends FunctionalComponent<infer P, any> ? Readonly<P> : {}

export type ComponentListeners<
C extends Component<any, any>
> = C extends ComponentOptionsBase<any, any, any, any, any, any, any, infer E>
? EmitListeners<E>
: C extends FunctionalComponent<any, infer E>
? EmitListeners<E>
: EmitListeners<string[]>
4 changes: 3 additions & 1 deletion packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ export {
export { EmitsOptions, ObjectEmitsOptions } from './componentEmits'
export {
ComponentPublicInstance,
ComponentCustomProperties
ComponentCustomProperties,
ComponentProps,
ComponentListeners
} from './componentPublicInstance'
export {
Renderer,
Expand Down
209 changes: 209 additions & 0 deletions test-dts/utilityTypes.test-d.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import {
FunctionalComponent,
ComponentListeners,
ComponentProps,
defineComponent,
expectType,
expectAssignable
} from './index'

describe('ComponentListeners', () => {
test('defineComponent', () => {
const C1 = defineComponent({})
type C1Listeners = ComponentListeners<typeof C1>
expectType<Record<string, (...args: any[]) => void>>({} as C1Listeners)

const C2 = defineComponent({
emits: ['foo', 'bar']
})
type C2Listeners = ComponentListeners<typeof C2>
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<typeof C3>
expectType<{
foo: () => void
bar: (n: number) => void
baz: (n: number, s: string) => void
}>({} as C3Listeners)
})

test('functionalComponent', () => {
const C1 = () => <div>Hello world</div>
type C1Listeners = ComponentListeners<typeof C1>
expectType<Record<string, (...args: any[]) => void>>({} as C1Listeners)

const C2: FunctionalComponent<{}, ['foo', 'bar']> = () => (
<div>Hello world</div>
)
type C2Listeners = ComponentListeners<typeof C2>
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> = () => (
<div>Hello world</div>
)
type C3Listeners = ComponentListeners<typeof C3>
expectType<ObjectEmits>({} as C3Listeners)
})
})

describe('ComponentProps', () => {
test('defineComponent', () => {
const C1 = defineComponent({})
type C1Props = ComponentProps<typeof C1>
expectAssignable<Required<C1Props>>({})

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<typeof C2>
expectType<{ readonly c2p1: any; readonly c2p2: any; readonly c2p3: any }>(
{} as Required<C2Props>
)
expectAssignable<C2Props['c2p1']>(undefined)
expectAssignable<C2Props['c2p2']>(undefined)
expectAssignable<C2Props['c2p3']>(undefined)

const C3 = defineComponent({
props: {
c3p1: String,
c3p2: { type: Number, required: true },
c3p3: { type: Boolean, default: true }
}
})
type C3Props = ComponentProps<typeof C3>
expectType<{
readonly c3p1: string
readonly c3p2: number
readonly c3p3: boolean
}>({} as Required<C3Props>)
expectAssignable<C3Props['c3p1']>(undefined)
expectAssignable<C3Props['c3p3']>(undefined)
// @ts-expect-error
expectAssignable<C3Props['c3p2']>(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<typeof C1>
expectType<CommonPropsExpected>({} as Required<C1Props>)
expectAssignable<C1Props['m1P1']>(undefined)
expectAssignable<C1Props['m2P1']>(undefined)
expectAssignable<C1Props['m2P3']>(undefined)
expectAssignable<C1Props['bP1']>(undefined)
// @ts-expect-error
expectAssignable<C1Props['m2P2']>(undefined)

const C2 = defineComponent({
extends: B,
mixins: [M1, M2],
props: ['c2P1']
})
type C2Props = ComponentProps<typeof C2>
expectType<
CommonPropsExpected & {
c2P1: any
}
>({} as Required<C2Props>)
expectAssignable<C2Props['m1P1']>(undefined)
expectAssignable<C2Props['m2P1']>(undefined)
expectAssignable<C2Props['m2P3']>(undefined)
expectAssignable<C2Props['bP1']>(undefined)
expectAssignable<C2Props['c2P1']>(undefined)
// @ts-expect-error
expectAssignable<C2Props['m2P2']>(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<typeof C3>
expectType<
CommonPropsExpected & {
c3P1: string
c3P2: number
c3P3: number
}
>({} as Required<C3Props>)
expectAssignable<C3Props['m1P1']>(undefined)
expectAssignable<C3Props['m2P1']>(undefined)
expectAssignable<C3Props['m2P3']>(undefined)
expectAssignable<C3Props['bP1']>(undefined)
expectAssignable<C3Props['c3P1']>(undefined)
expectAssignable<C3Props['c3P3']>(undefined)
// @ts-expect-error
expectAssignable<C3Props['m2P2']>(undefined)
// @ts-expect-error
expectAssignable<C3Props['c3P2']>(undefined)
})

test('functionalComponent', () => {
const C1 = () => <div>Hello world</div>
type C1Props = ComponentProps<typeof C1>
expectType<never>({} as keyof C1Props)

type C2PropsExpected = { name: string; age?: number }
const C2: FunctionalComponent<C2PropsExpected> = props => (
<div>Hello {props.name}</div>
)
type C2Props = ComponentProps<typeof C2>
expectType<Required<C2PropsExpected>>({} as Required<C2Props>)
// @ts-expect-error
expectAssignable<C2Props['name']>(undefined)
expectAssignable<C2Props['age']>(undefined)
})
})

0 comments on commit d1ae065

Please sign in to comment.