Skip to content

feat(types): add utility types (#2094) #2179

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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)
})
})