]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: improve props typing
authorEvan You <yyx990803@gmail.com>
Fri, 31 May 2019 16:47:05 +0000 (00:47 +0800)
committerEvan You <yyx990803@gmail.com>
Fri, 31 May 2019 16:47:05 +0000 (00:47 +0800)
packages/runtime-core/__tests__/createComponent.spec.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/index.ts

index 9821a28501420d1711729c53824307918b86b822..8afefcdc6612423cebc0d71ef477f7fdf6281e30 100644 (file)
@@ -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
+//     }
+//   })
+// })
index f13d9ac46d838b79c02a3c65a96605aa84c9ce6b..5ae92c9f105f86957b83e814663c47ce8f0dda1f 100644 (file)
@@ -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
 }
index 35fafc8ae4025152109f8e6e68a433f331324bbb..7e4fac1051fe2633111ed01f39f1168341421ecb 100644 (file)
@@ -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)) {
index df111f70637136fe8ce847065b8faadc6b57d61b..f7ce3941d4a0491fa4c28f5820a7fa8d87494bd4 100644 (file)
@@ -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'