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: {
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
+// }
+// })
+// })
export type Data = { [key: string]: any }
-export type ComponentPublicProperties<P = Data, S = Data> = {
+export type ComponentPublicProperties<P = {}, S = {}> = {
$state: S
$props: P
$attrs: 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
}
} & 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
}
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
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>
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.`)
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)) {
export { Slot, Slots } from './componentSlots'
-export { ComponentPropsOptions } from './componentProps'
+export { PropType, ComponentPropsOptions } from './componentProps'
export * from './reactivity'
export * from './componentLifecycle'