Skip to content

Commit

Permalink
fix(types): prop type infer, fix #555 (#561)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mister-Hope committed Oct 17, 2020
1 parent ded5ab7 commit 35f8fec
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 24 deletions.
37 changes: 15 additions & 22 deletions src/component/componentProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ export type ComponentObjectPropsOptions<P = Data> = {
[K in keyof P]: Prop<P[K]> | null
}

export type Prop<T> = PropOptions<T> | PropType<T>
export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>

type DefaultFactory<T> = () => T | null | undefined

export interface PropOptions<T = any> {
export interface PropOptions<T = any, D = T> {
type?: PropType<T> | true | null
required?: boolean
default?: T | DefaultFactory<T> | null | undefined
default?: D | DefaultFactory<D> | null | undefined | object
validator?(value: unknown): boolean
}

Expand All @@ -26,18 +26,11 @@ type PropConstructor<T> =
| { (): T }
| { new (...args: string[]): Function }

type RequiredKeys<T, MakeDefaultRequired> = {
[K in keyof T]: T[K] extends
| { required: true }
| (MakeDefaultRequired extends true ? { default: any } : never)
? K
: never
type RequiredKeys<T> = {
[K in keyof T]: T[K] extends { required: true } | { default: any } ? K : never
}[keyof T]

type OptionalKeys<T, MakeDefaultRequired> = Exclude<
keyof T,
RequiredKeys<T, MakeDefaultRequired>
>
type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>

type ExtractFunctionPropType<
T extends Function,
Expand All @@ -55,18 +48,18 @@ type InferPropType<T> = T extends null
: T extends { type: null | true }
? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean`
: T extends ObjectConstructor | { type: ObjectConstructor }
? { [key: string]: any }
? Record<string, any>
: T extends BooleanConstructor | { type: BooleanConstructor }
? boolean
: T extends FunctionConstructor
? Function
: T extends Prop<infer V>
? ExtractCorrectPropType<V> : T;
: T extends Prop<infer V, infer D>
? unknown extends V
? D
: ExtractCorrectPropType<V>
: T

export type ExtractPropTypes<
O,
MakeDefaultRequired extends boolean = true
> = O extends object
? { [K in RequiredKeys<O, MakeDefaultRequired>]: InferPropType<O[K]> } &
{ [K in OptionalKeys<O, MakeDefaultRequired>]?: InferPropType<O[K]> }
export type ExtractPropTypes<O> = O extends object
? { [K in RequiredKeys<O>]: InferPropType<O[K]> } &
{ [K in OptionalKeys<O>]?: InferPropType<O[K]> }
: { [K in string]: any }
4 changes: 2 additions & 2 deletions src/component/componentProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type VueConstructorProxy<PropsOptions, RawBindings> = VueConstructor & {
new (...args: any[]): ComponentRenderProxy<
ExtractPropTypes<PropsOptions>,
ShallowUnwrapRef<RawBindings>,
ExtractPropTypes<PropsOptions, false>
ExtractPropTypes<PropsOptions>
>
}

Expand All @@ -59,6 +59,6 @@ export type VueProxy<
Methods,
Computed,
PropsOptions,
ExtractPropTypes<PropsOptions, false>
ExtractPropTypes<PropsOptions>
> &
VueConstructorProxy<PropsOptions, RawBindings>
38 changes: 38 additions & 0 deletions test-dts/defineComponent.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('with object props', () => {
b: string
e?: Function
bb: string
bbb: string
cc?: string[] | undefined
dd: { n: 1 }
ee?: () => string
Expand All @@ -23,6 +24,9 @@ describe('with object props', () => {
eee: () => { a: string }
fff: (a: number, b: string) => { a: boolean }
hhh: boolean
ggg: 'foo' | 'bar'
ffff: (a: number, b: string) => { a: boolean }
validated?: string
}

type GT = string & { __brand: unknown }
Expand All @@ -40,6 +44,11 @@ describe('with object props', () => {
bb: {
default: 'hello',
},
bbb: {
// Note: default function value requires arrow syntax + explicit
// annotation
default: (props: any) => (props.bb as string) || 'foo',
},
// explicit type casting
cc: Array as PropType<string[]>,
// required + type casting
Expand Down Expand Up @@ -68,10 +77,25 @@ describe('with object props', () => {
type: Function as PropType<(a: number, b: string) => { a: boolean }>,
required: true,
},
// default + type casting
ggg: {
type: String as PropType<'foo' | 'bar'>,
default: 'foo',
},
hhh: {
type: Boolean,
required: true,
},
// default + function
ffff: {
type: Function as PropType<(a: number, b: string) => { a: boolean }>,
default: (_a: number, _b: string) => ({ a: true }),
},
validated: {
type: String,
// validator requires explicit annotation
validator: (val: unknown) => val !== '',
},
},
setup(props) {
// type assertion. See https://github.com/SamVerschueren/tsd
Expand All @@ -83,11 +107,15 @@ describe('with object props', () => {
expectType<ExpectedProps['dd']>(props.dd)
expectType<ExpectedProps['ee']>(props.ee)
expectType<ExpectedProps['ff']>(props.ff)
expectType<ExpectedProps['bbb']>(props.bbb)
expectType<ExpectedProps['ccc']>(props.ccc)
expectType<ExpectedProps['ddd']>(props.ddd)
expectType<ExpectedProps['eee']>(props.eee)
expectType<ExpectedProps['fff']>(props.fff)
expectType<ExpectedProps['ggg']>(props.ggg)
expectType<ExpectedProps['hhh']>(props.hhh)
expectType<ExpectedProps['ffff']>(props.ffff)
expectType<ExpectedProps['validated']>(props.validated)

isNotAnyOrUndefined(props.a)
isNotAnyOrUndefined(props.b)
Expand All @@ -97,11 +125,14 @@ describe('with object props', () => {
isNotAnyOrUndefined(props.dd)
isNotAnyOrUndefined(props.ee)
isNotAnyOrUndefined(props.ff)
isNotAnyOrUndefined(props.bbb)
isNotAnyOrUndefined(props.ccc)
isNotAnyOrUndefined(props.ddd)
isNotAnyOrUndefined(props.eee)
isNotAnyOrUndefined(props.fff)
isNotAnyOrUndefined(props.ggg)
isNotAnyOrUndefined(props.hhh)
isNotAnyOrUndefined(props.ffff)

expectError((props.a = 1))

Expand All @@ -126,11 +157,15 @@ describe('with object props', () => {
expectType<ExpectedProps['dd']>(props.dd)
expectType<ExpectedProps['ee']>(props.ee)
expectType<ExpectedProps['ff']>(props.ff)
expectType<ExpectedProps['bbb']>(props.bbb)
expectType<ExpectedProps['ccc']>(props.ccc)
expectType<ExpectedProps['ddd']>(props.ddd)
expectType<ExpectedProps['eee']>(props.eee)
expectType<ExpectedProps['fff']>(props.fff)
expectType<ExpectedProps['ggg']>(props.ggg)
expectType<ExpectedProps['hhh']>(props.hhh)
expectType<ExpectedProps['ffff']>(props.ffff)
expectType<ExpectedProps['validated']>(props.validated)

// @ts-expect-error props should be readonly
expectError((props.a = 1))
Expand All @@ -144,10 +179,13 @@ describe('with object props', () => {
expectType<ExpectedProps['dd']>(this.dd)
expectType<ExpectedProps['ee']>(this.ee)
expectType<ExpectedProps['ff']>(this.ff)
expectType<ExpectedProps['bbb']>(this.bbb)
expectType<ExpectedProps['ccc']>(this.ccc)
expectType<ExpectedProps['ddd']>(this.ddd)
expectType<ExpectedProps['eee']>(this.eee)
expectType<ExpectedProps['fff']>(this.fff)
expectType<ExpectedProps['ggg']>(this.ggg)
expectType<ExpectedProps['ffff']>(this.ffff)
expectType<ExpectedProps['hhh']>(this.hhh)

// @ts-expect-error props on `this` should be readonly
Expand Down

0 comments on commit 35f8fec

Please sign in to comment.