wip: improve props typing
This commit is contained in:
parent
c0c06813a7
commit
da5edd3429
@ -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<string[]>,
|
||||
// required + type casting
|
||||
dd: {
|
||||
type: (Array as any) as PropType<string[]>,
|
||||
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
|
||||
// ;(<MyComponent a={1} b="foo"/>)
|
||||
})
|
||||
|
||||
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
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
@ -13,7 +13,7 @@ import { Slots } from './componentSlots'
|
||||
|
||||
export type Data = { [key: string]: any }
|
||||
|
||||
export type ComponentPublicProperties<P = Data, S = Data> = {
|
||||
export type ComponentPublicProperties<P = {}, S = {}> = {
|
||||
$state: S
|
||||
$props: P
|
||||
$attrs: Data
|
||||
@ -27,16 +27,16 @@ export type ComponentPublicProperties<P = Data, S = Data> = {
|
||||
} & P &
|
||||
S
|
||||
|
||||
export interface ComponentOptions<
|
||||
interface ComponentOptions<
|
||||
RawProps = ComponentPropsOptions,
|
||||
RawBindings = Data | void,
|
||||
RawBindings = Data,
|
||||
Props = ExtractPropTypes<RawProps>,
|
||||
Bindings = UnwrapValue<RawBindings>
|
||||
ExposedProps = RawProps extends object ? Props : {}
|
||||
> {
|
||||
props?: RawProps
|
||||
setup?: (props: Props) => RawBindings
|
||||
render?: <State extends Bindings>(
|
||||
this: ComponentPublicProperties<Props, State>,
|
||||
setup?: (this: ComponentPublicProperties, props: Props) => RawBindings
|
||||
render?: <State extends UnwrapValue<RawBindings>>(
|
||||
this: ComponentPublicProperties<ExposedProps, State>,
|
||||
ctx: ComponentInstance<Props, State>
|
||||
) => VNodeChild
|
||||
}
|
||||
@ -81,16 +81,11 @@ export type ComponentInstance<P = Data, S = Data> = {
|
||||
} & LifecycleHooks
|
||||
|
||||
// no-op, for type inference only
|
||||
export function createComponent<
|
||||
RawProps,
|
||||
RawBindings,
|
||||
Props = ExtractPropTypes<RawProps>,
|
||||
Bindings = UnwrapValue<RawBindings>
|
||||
>(
|
||||
options: ComponentOptions<RawProps, RawBindings, Props, Bindings>
|
||||
export function createComponent<RawProps, RawBindings>(
|
||||
options: ComponentOptions<RawProps, RawBindings>
|
||||
): {
|
||||
// for TSX
|
||||
new (): { $props: Props }
|
||||
new (): { $props: ExtractPropTypes<RawProps> }
|
||||
} {
|
||||
return options as any
|
||||
}
|
||||
|
@ -13,14 +13,10 @@ import { warn } from './warning'
|
||||
import { Data, ComponentInstance } from './component'
|
||||
|
||||
export type ComponentPropsOptions<P = Data> = {
|
||||
[K in keyof P]: PropValidator<P[K]>
|
||||
[K in keyof P]: Prop<P[K]> | null
|
||||
}
|
||||
|
||||
type Prop<T> = { (): T } | { new (...args: any[]): T & object }
|
||||
|
||||
type PropType<T> = Prop<T> | Prop<T>[]
|
||||
|
||||
type PropValidator<T> = PropOptions<T> | PropType<T>
|
||||
type Prop<T> = PropOptions<T> | PropType<T>
|
||||
|
||||
interface PropOptions<T = any> {
|
||||
type?: PropType<T> | true | null
|
||||
@ -29,23 +25,42 @@ interface PropOptions<T = any> {
|
||||
validator?(value: any): boolean
|
||||
}
|
||||
|
||||
export type ExtractPropTypes<PropOptions> = {
|
||||
readonly [key in keyof PropOptions]: PropOptions[key] extends PropValidator<
|
||||
infer V
|
||||
>
|
||||
? V
|
||||
: PropOptions[key] extends null | true ? any : PropOptions[key]
|
||||
}
|
||||
export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
|
||||
|
||||
type PropConstructor<T> = { new (...args: any[]): T & object } | { (): T }
|
||||
|
||||
type RequiredKeys<T> = {
|
||||
[K in keyof T]: T[K] extends { required: true } | { default: any } ? K : never
|
||||
}[keyof T]
|
||||
|
||||
type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>
|
||||
|
||||
type InferPropType<T> = 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<infer V> ? V : T
|
||||
|
||||
export type ExtractPropTypes<O> = O extends object
|
||||
? { readonly [K in RequiredKeys<O>]: InferPropType<O[K]> } &
|
||||
{ readonly [K in OptionalKeys<O>]?: InferPropType<O[K]> }
|
||||
: { [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<string, NormalizedProp>
|
||||
|
||||
@ -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<any>): AssertionResult {
|
||||
function assertType(value: any, type: PropConstructor<any>): AssertionResult {
|
||||
let valid
|
||||
const expectedType = getType(type)
|
||||
if (simpleCheckRE.test(expectedType)) {
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user