From da5edd3429c59cbfb5d969eaebed8e6b46d69edc Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 1 Jun 2019 00:47:05 +0800 Subject: [PATCH] wip: improve props typing --- .../__tests__/createComponent.spec.ts | 77 +++++++++++++++++-- packages/runtime-core/src/component.ts | 25 +++--- packages/runtime-core/src/componentProps.ts | 60 +++++++++------ packages/runtime-core/src/index.ts | 2 +- 4 files changed, 118 insertions(+), 46 deletions(-) diff --git a/packages/runtime-core/__tests__/createComponent.spec.ts b/packages/runtime-core/__tests__/createComponent.spec.ts index 9821a285..8afefcdc 100644 --- a/packages/runtime-core/__tests__/createComponent.spec.ts +++ b/packages/runtime-core/__tests__/createComponent.spec.ts @@ -1,17 +1,34 @@ import { createComponent } from '../src/component' import { value } from '@vue/observer' +import { PropType } from '../src/componentProps' test('createComponent type inference', () => { - const MyComponent = createComponent({ + createComponent({ props: { a: Number, + // required should make property non-void b: { - type: String + type: String, + required: true + }, + // default value should infer type and make it non-void + bb: { + default: 'hello' + }, + // explicit type casting + cc: (Array as any) as PropType, + // required + type casting + dd: { + type: (Array as any) as PropType, + required: true } - }, + } as const, // required to narrow for conditional check setup(props) { - props.a * 2 + props.a && props.a * 2 props.b.slice() + props.bb.slice() + props.cc && props.cc.push('hoo') + props.dd.push('dd') return { c: value(1), d: { @@ -22,15 +39,61 @@ test('createComponent type inference', () => { render({ state, props }) { state.c * 2 state.d.e.slice() - props.a * 2 + props.a && props.a * 2 props.b.slice() - this.a * 2 + props.bb.slice() + props.cc && props.cc.push('hoo') + props.dd.push('dd') + this.a && this.a * 2 this.b.slice() + this.bb.slice() this.c * 2 this.d.e.slice() + this.cc && this.cc.push('hoo') + this.dd.push('dd') } }) - MyComponent // avoid unused // rename this file to .tsx to test TSX props inference // ;() }) + +test('type inference w/ optional props declaration', () => { + createComponent({ + setup(props) { + props.anything + return { + a: 1 + } + }, + render({ props, state }) { + props.foobar + state.a * 2 + this.a * 2 + + // should not make state and this indexable + // state.foobar + // this.foobar + } + }) +}) + +// test('type inference w/ array props declaration', () => { +// createComponent({ +// props: ['a', 'b'], +// setup(props) { +// props.a +// props.b +// return { +// c: 1 +// } +// }, +// render({ props, state }) { +// props.a +// props.b +// state.c +// this.a +// this.b +// this.c +// } +// }) +// }) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index f13d9ac4..5ae92c9f 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -13,7 +13,7 @@ import { Slots } from './componentSlots' export type Data = { [key: string]: any } -export type ComponentPublicProperties

= { +export type ComponentPublicProperties

= { $state: S $props: P $attrs: Data @@ -27,16 +27,16 @@ export type ComponentPublicProperties

= { } & P & S -export interface ComponentOptions< +interface ComponentOptions< RawProps = ComponentPropsOptions, - RawBindings = Data | void, + RawBindings = Data, Props = ExtractPropTypes, - Bindings = UnwrapValue + ExposedProps = RawProps extends object ? Props : {} > { props?: RawProps - setup?: (props: Props) => RawBindings - render?: ( - this: ComponentPublicProperties, + setup?: (this: ComponentPublicProperties, props: Props) => RawBindings + render?: >( + this: ComponentPublicProperties, ctx: ComponentInstance ) => VNodeChild } @@ -81,16 +81,11 @@ export type ComponentInstance

= { } & LifecycleHooks // no-op, for type inference only -export function createComponent< - RawProps, - RawBindings, - Props = ExtractPropTypes, - Bindings = UnwrapValue ->( - options: ComponentOptions +export function createComponent( + options: ComponentOptions ): { // for TSX - new (): { $props: Props } + new (): { $props: ExtractPropTypes } } { return options as any } diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index 35fafc8a..7e4fac10 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -13,14 +13,10 @@ import { warn } from './warning' import { Data, ComponentInstance } from './component' export type ComponentPropsOptions

= { - [K in keyof P]: PropValidator + [K in keyof P]: Prop | null } -type Prop = { (): T } | { new (...args: any[]): T & object } - -type PropType = Prop | Prop[] - -type PropValidator = PropOptions | PropType +type Prop = PropOptions | PropType interface PropOptions { type?: PropType | true | null @@ -29,23 +25,42 @@ interface PropOptions { validator?(value: any): boolean } -export type ExtractPropTypes = { - readonly [key in keyof PropOptions]: PropOptions[key] extends PropValidator< - infer V - > - ? V - : PropOptions[key] extends null | true ? any : PropOptions[key] -} +export type PropType = PropConstructor | PropConstructor[] + +type PropConstructor = { new (...args: any[]): T & object } | { (): T } + +type RequiredKeys = { + [K in keyof T]: T[K] extends { required: true } | { default: any } ? K : never +}[keyof T] + +type OptionalKeys = Exclude> + +type InferPropType = T extends null + ? any + : // null & true would fail to infer + T extends { type: null | true } + ? any + : // somehow `ObjectContructor` when inferred from { (): T } becomes `any` + T extends ObjectConstructor | { type: ObjectConstructor } + ? { [key: string]: any } + : T extends Prop ? V : T + +export type ExtractPropTypes = O extends object + ? { readonly [K in RequiredKeys]: InferPropType } & + { readonly [K in OptionalKeys]?: InferPropType } + : { [K in string]: any } const enum BooleanFlags { shouldCast = '1', shouldCastTrue = '2' } -type NormalizedProp = PropOptions & { - [BooleanFlags.shouldCast]?: boolean - [BooleanFlags.shouldCastTrue]?: boolean -} +type NormalizedProp = + | null + | (PropOptions & { + [BooleanFlags.shouldCast]?: boolean + [BooleanFlags.shouldCastTrue]?: boolean + }) type NormalizedPropsOptions = Record @@ -181,14 +196,13 @@ function normalizePropsOptions( const normalizedKey = camelize(key) if (!isReservedKey(normalizedKey)) { const opt = raw[key] - const prop = (normalized[normalizedKey] = + const prop: NormalizedProp = (normalized[normalizedKey] = isArray(opt) || isFunction(opt) ? { type: opt } : opt) - if (prop) { + if (prop != null) { const booleanIndex = getTypeIndex(Boolean, prop.type) const stringIndex = getTypeIndex(String, prop.type) - ;(prop as NormalizedProp)[BooleanFlags.shouldCast] = booleanIndex > -1 - ;(prop as NormalizedProp)[BooleanFlags.shouldCastTrue] = - booleanIndex < stringIndex + prop[BooleanFlags.shouldCast] = booleanIndex > -1 + prop[BooleanFlags.shouldCastTrue] = booleanIndex < stringIndex } } else if (__DEV__) { warn(`Invalid prop name: "${normalizedKey}" is a reserved property.`) @@ -270,7 +284,7 @@ function validateProp( const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/ -function assertType(value: any, type: Prop): AssertionResult { +function assertType(value: any, type: PropConstructor): AssertionResult { let valid const expectedType = getType(type) if (simpleCheckRE.test(expectedType)) { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index df111f70..f7ce3941 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -15,7 +15,7 @@ export { export { Slot, Slots } from './componentSlots' -export { ComponentPropsOptions } from './componentProps' +export { PropType, ComponentPropsOptions } from './componentProps' export * from './reactivity' export * from './componentLifecycle'