]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(types): mixins/extends support in TypeScript (#626)
authordoly mood <dolymood@gmail.com>
Tue, 9 Jun 2020 14:37:00 +0000 (22:37 +0800)
committerGitHub <noreply@github.com>
Tue, 9 Jun 2020 14:37:00 +0000 (10:37 -0400)
packages/reactivity/__tests__/collections/Set.spec.ts
packages/reactivity/src/baseHandlers.ts
packages/runtime-core/__tests__/apiOptions.spec.ts
packages/runtime-core/src/apiDefineComponent.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentEmits.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/helpers/typeUtils.ts [new file with mode: 0644]
test-dts/defineComponent.test-d.tsx

index cdccd347999df3a771b6f0d508bfe6a5c1eb26c0..8164081df5e9a6911bb887e886f4d9c704435e5d 100644 (file)
@@ -412,13 +412,13 @@ describe('reactivity/collections', () => {
         `Reactive Set contains both the raw and reactive`
       ).toHaveBeenWarned()
     })
-    
+
     it('thisArg', () => {
-      const raw = new Set([ 'value' ])
+      const raw = new Set(['value'])
       const proxy = reactive(raw)
       const thisArg = {}
       let count = 0
-      proxy.forEach(function (this :{}, value, _, set) {
+      proxy.forEach(function(this: {}, value, _, set) {
         ++count
         expect(this).toBe(thisArg)
         expect(value).toBe('value')
index 5bebe564016b1cc3d32952ba164f93ebf3dbb6de..3d5d5635acbccfed44e5b8d57a6a57f45e321d46 100644 (file)
@@ -49,7 +49,7 @@ function createGetter(isReadonly = false, shallow = false) {
     }
     const res = Reflect.get(target, key, receiver)
 
-    if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
+    if ((isSymbol(key) && builtInSymbols.has(key)) || key === '__proto__') {
       return res
     }
 
index da99dd4a3ab48261605407d23f0a0bd1b765f109..0107848ec68ecb5a82014d3352480dcddb5dfc33 100644 (file)
@@ -443,6 +443,11 @@ describe('api: options', () => {
       }
     }
     const mixinB = {
+      props: {
+        bP: {
+          type: String
+        }
+      },
       data() {
         return {
           b: 2
@@ -452,40 +457,65 @@ describe('api: options', () => {
         calls.push('mixinB created')
         expect(this.a).toBe(1)
         expect(this.b).toBe(2)
+        expect(this.bP).toBeUndefined()
         expect(this.c).toBe(3)
+        expect(this.cP1).toBeUndefined()
       },
       mounted() {
         calls.push('mixinB mounted')
       }
     }
-    const Comp = {
-      mixins: [mixinA, mixinB],
+    const mixinC = defineComponent({
+      props: ['cP1', 'cP2'],
       data() {
         return {
           c: 3
         }
       },
-      created(this: any) {
+      created() {
+        calls.push('mixinC created')
+        expect(this.c).toBe(3)
+        expect(this.cP1).toBeUndefined()
+      },
+      mounted() {
+        calls.push('mixinC mounted')
+      }
+    })
+    const Comp = defineComponent({
+      props: {
+        aaa: String
+      },
+      mixins: [defineComponent(mixinA), defineComponent(mixinB), mixinC],
+      data() {
+        return {
+          z: 4
+        }
+      },
+      created() {
         calls.push('comp created')
         expect(this.a).toBe(1)
         expect(this.b).toBe(2)
+        expect(this.bP).toBeUndefined()
         expect(this.c).toBe(3)
+        expect(this.cP2).toBeUndefined()
+        expect(this.z).toBe(4)
       },
       mounted() {
         calls.push('comp mounted')
       },
-      render(this: any) {
+      render() {
         return `${this.a}${this.b}${this.c}`
       }
-    }
-
+    })
     expect(renderToString(h(Comp))).toBe(`123`)
     expect(calls).toEqual([
       'mixinA created',
       'mixinB created',
+      'mixinC created',
       'comp created',
       'mixinA mounted',
       'mixinB mounted',
+      'mixinC mounted',
       'comp mounted'
     ])
   })
@@ -498,12 +528,17 @@ describe('api: options', () => {
           a: 1
         }
       },
-      mounted() {
+      methods: {
+        sayA() {}
+      },
+      mounted(this: any) {
+        expect(this.a).toBe(1)
+        expect(this.b).toBe(2)
         calls.push('base')
       }
     }
-    const Comp = {
-      extends: Base,
+    const Comp = defineComponent({
+      extends: defineComponent(Base),
       data() {
         return {
           b: 2
@@ -512,15 +547,66 @@ describe('api: options', () => {
       mounted() {
         calls.push('comp')
       },
-      render(this: any) {
+      render() {
         return `${this.a}${this.b}`
       }
-    }
+    })
 
     expect(renderToString(h(Comp))).toBe(`12`)
     expect(calls).toEqual(['base', 'comp'])
   })
 
+  test('extends with mixins', () => {
+    const calls: string[] = []
+    const Base = {
+      data() {
+        return {
+          a: 1
+        }
+      },
+      methods: {
+        sayA() {}
+      },
+      mounted(this: any) {
+        expect(this.a).toBe(1)
+        expect(this.b).toBeTruthy()
+        expect(this.c).toBe(2)
+        calls.push('base')
+      }
+    }
+    const Base2 = {
+      data() {
+        return {
+          b: true
+        }
+      },
+      mounted(this: any) {
+        expect(this.a).toBe(1)
+        expect(this.b).toBeTruthy()
+        expect(this.c).toBe(2)
+        calls.push('base2')
+      }
+    }
+    const Comp = defineComponent({
+      extends: defineComponent(Base),
+      mixins: [defineComponent(Base2)],
+      data() {
+        return {
+          c: 2
+        }
+      },
+      mounted() {
+        calls.push('comp')
+      },
+      render() {
+        return `${this.a}${this.b}${this.c}`
+      }
+    })
+
+    expect(renderToString(h(Comp))).toBe(`1true2`)
+    expect(calls).toEqual(['base', 'base2', 'comp'])
+  })
+
   test('accessing setup() state from options', async () => {
     const Comp = defineComponent({
       setup() {
index fb945bcb4379b501dde18a161dd308672752313d..959b10bdf1b5016d5f73c3cdeb12b4ab42c9e705 100644 (file)
@@ -4,10 +4,14 @@ import {
   ComponentOptionsWithoutProps,
   ComponentOptionsWithArrayProps,
   ComponentOptionsWithObjectProps,
+  ComponentOptionsMixin,
   RenderFunction
 } from './componentOptions'
 import { SetupContext, FunctionalComponent } from './component'
-import { ComponentPublicInstance } from './componentProxy'
+import {
+  CreateComponentPublicInstance,
+  ComponentPublicInstanceConstructor
+} from './componentProxy'
 import { ExtractPropTypes, ComponentPropsOptions } from './componentProps'
 import { EmitsOptions } from './componentEmits'
 import { isFunction } from '@vue/shared'
@@ -25,17 +29,21 @@ export function defineComponent<Props, RawBindings = object>(
     props: Readonly<Props>,
     ctx: SetupContext
   ) => RawBindings | RenderFunction
-): {
-  new (): ComponentPublicInstance<
+): ComponentPublicInstanceConstructor<
+  CreateComponentPublicInstance<
     Props,
     RawBindings,
     {},
     {},
     {},
+    {},
+    {},
+    {},
     // public props
     VNodeProps & Props
   >
-} & FunctionalComponent<Props>
+> &
+  FunctionalComponent<Props>
 
 // overload 2: object format with no props
 // (uses user defined props interface)
@@ -46,21 +54,46 @@ export function defineComponent<
   D = {},
   C extends ComputedOptions = {},
   M extends MethodOptions = {},
+  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
+  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
   E extends EmitsOptions = Record<string, any>,
   EE extends string = string
 >(
-  options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M, E, EE>
-): {
-  new (): ComponentPublicInstance<
+  options: ComponentOptionsWithoutProps<
     Props,
     RawBindings,
     D,
     C,
     M,
+    Mixin,
+    Extends,
+    E,
+    EE
+  >
+): ComponentPublicInstanceConstructor<
+  CreateComponentPublicInstance<
+    Props,
+    RawBindings,
+    D,
+    C,
+    M,
+    Mixin,
+    Extends,
     E,
     VNodeProps & Props
   >
-} & ComponentOptionsWithoutProps<Props, RawBindings, D, C, M, E, EE>
+> &
+  ComponentOptionsWithoutProps<
+    Props,
+    RawBindings,
+    D,
+    C,
+    M,
+    Mixin,
+    Extends,
+    E,
+    EE
+  >
 
 // overload 3: object format with array props declaration
 // props inferred as { [key in PropNames]?: any }
@@ -71,6 +104,8 @@ export function defineComponent<
   D,
   C extends ComputedOptions = {},
   M extends MethodOptions = {},
+  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
+  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
   E extends EmitsOptions = Record<string, any>,
   EE extends string = string
 >(
@@ -80,13 +115,36 @@ export function defineComponent<
     D,
     C,
     M,
+    Mixin,
+    Extends,
+    E,
+    EE
+  >
+): ComponentPublicInstanceConstructor<
+  // array props technically doesn't place any contraints on props in TSX before,
+  // but now we can export array props in TSX
+  CreateComponentPublicInstance<
+    Readonly<{ [key in PropNames]?: any }>,
+    RawBindings,
+    D,
+    C,
+    M,
+    Mixin,
+    Extends,
+    E
+  >
+> &
+  ComponentOptionsWithArrayProps<
+    PropNames,
+    RawBindings,
+    D,
+    C,
+    M,
+    Mixin,
+    Extends,
     E,
     EE
   >
-): {
-  // array props technically doesn't place any constraints on props in TSX
-  new (): ComponentPublicInstance<VNodeProps, RawBindings, D, C, M, E>
-} & ComponentOptionsWithArrayProps<PropNames, RawBindings, D, C, M, E, EE>
 
 // overload 4: object format with object props declaration
 // see `ExtractPropTypes` in ./componentProps.ts
@@ -98,6 +156,8 @@ export function defineComponent<
   D,
   C extends ComputedOptions = {},
   M extends MethodOptions = {},
+  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
+  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
   E extends EmitsOptions = Record<string, any>,
   EE extends string = string
 >(
@@ -107,20 +167,35 @@ export function defineComponent<
     D,
     C,
     M,
+    Mixin,
+    Extends,
     E,
     EE
   >
-): {
-  new (): ComponentPublicInstance<
+): ComponentPublicInstanceConstructor<
+  CreateComponentPublicInstance<
     ExtractPropTypes<PropsOptions>,
     RawBindings,
     D,
     C,
     M,
+    Mixin,
+    Extends,
     E,
     VNodeProps & ExtractPropTypes<PropsOptions, false>
   >
-} & ComponentOptionsWithObjectProps<PropsOptions, RawBindings, D, C, M, E, EE>
+> &
+  ComponentOptionsWithObjectProps<
+    PropsOptions,
+    RawBindings,
+    D,
+    C,
+    M,
+    Mixin,
+    Extends,
+    E,
+    EE
+  >
 
 // implementation, close to no-op
 export function defineComponent(options: unknown) {
index 89d9916f4a11b5f16cd6941bd0fc1addd49cd323..522e64570d425d8fd06f756cef207ede65b830a0 100644 (file)
@@ -7,6 +7,7 @@ import {
   shallowReadonly
 } from '@vue/reactivity'
 import {
+  CreateComponentPublicInstance,
   ComponentPublicInstance,
   PublicInstanceProxyHandlers,
   RuntimeCompiledPublicInstanceProxyHandlers,
@@ -96,7 +97,15 @@ export type Component = ComponentOptions | FunctionalComponent<any>
 // The constructor type is an artificial type returned by defineComponent().
 export type PublicAPIComponent =
   | Component
-  | { new (...args: any[]): ComponentPublicInstance<any, any, any, any, any> }
+  | {
+      new (...args: any[]): CreateComponentPublicInstance<
+        any,
+        any,
+        any,
+        any,
+        any
+      >
+    }
 
 export { ComponentOptions }
 
index 7b91bf3469228b6e9815c5e130d11596477ca0bf..28a6af654e57c9a234d7b17662e60ef73767def0 100644 (file)
@@ -12,6 +12,7 @@ import { ComponentInternalInstance } from './component'
 import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
 import { warn } from './warning'
 import { normalizePropsOptions } from './componentProps'
+import { UnionToIntersection } from './helpers/typeUtils'
 
 export type ObjectEmitsOptions = Record<
   string,
@@ -19,12 +20,6 @@ export type ObjectEmitsOptions = Record<
 >
 export type EmitsOptions = ObjectEmitsOptions | string[]
 
-type UnionToIntersection<U> = (U extends any
-  ? (k: U) => void
-  : never) extends ((k: infer I) => void)
-  ? I
-  : never
-
 export type EmitFn<
   Options = ObjectEmitsOptions,
   Event extends keyof Options = keyof Options
index b04b37fc803a8686c8f56d4a7d547087cd142518..0cab6fc0cc8e16b91ea6d6a41d85acc16eb46e68 100644 (file)
@@ -48,7 +48,10 @@ import {
 } from './componentProps'
 import { EmitsOptions } from './componentEmits'
 import { Directive } from './directives'
-import { ComponentPublicInstance } from './componentProxy'
+import {
+  CreateComponentPublicInstance,
+  ComponentPublicInstance
+} from './componentProxy'
 import { warn } from './warning'
 import { VNodeChild } from './vnode'
 
@@ -78,10 +81,12 @@ export interface ComponentOptionsBase<
   D,
   C extends ComputedOptions,
   M extends MethodOptions,
+  Mixin extends ComponentOptionsMixin,
+  Extends extends ComponentOptionsMixin,
   E extends EmitsOptions,
   EE extends string = string
 >
-  extends LegacyOptions<Props, D, C, M>,
+  extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
     SFCInternalOptions,
     ComponentCustomOptions {
   setup?: (
@@ -148,12 +153,24 @@ export type ComponentOptionsWithoutProps<
   D = {},
   C extends ComputedOptions = {},
   M extends MethodOptions = {},
+  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
+  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
   E extends EmitsOptions = EmitsOptions,
   EE extends string = string
-> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
+> = ComponentOptionsBase<Props, RawBindings, D, C, M, Mixin, Extends, E, EE> & {
   props?: undefined
 } & ThisType<
-    ComponentPublicInstance<{}, RawBindings, D, C, M, E, Readonly<Props>>
+    CreateComponentPublicInstance<
+      {},
+      RawBindings,
+      D,
+      C,
+      M,
+      Mixin,
+      Extends,
+      E,
+      Readonly<Props>
+    >
   >
 
 export type ComponentOptionsWithArrayProps<
@@ -162,12 +179,25 @@ export type ComponentOptionsWithArrayProps<
   D = {},
   C extends ComputedOptions = {},
   M extends MethodOptions = {},
+  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
+  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
   E extends EmitsOptions = EmitsOptions,
   EE extends string = string,
   Props = Readonly<{ [key in PropNames]?: any }>
-> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
+> = ComponentOptionsBase<Props, RawBindings, D, C, M, Mixin, Extends, E, EE> & {
   props: PropNames[]
-} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
+} & ThisType<
+    CreateComponentPublicInstance<
+      Props,
+      RawBindings,
+      D,
+      C,
+      M,
+      Mixin,
+      Extends,
+      E
+    >
+  >
 
 export type ComponentOptionsWithObjectProps<
   PropsOptions = ComponentObjectPropsOptions,
@@ -175,18 +205,43 @@ export type ComponentOptionsWithObjectProps<
   D = {},
   C extends ComputedOptions = {},
   M extends MethodOptions = {},
+  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
+  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
   E extends EmitsOptions = EmitsOptions,
   EE extends string = string,
   Props = Readonly<ExtractPropTypes<PropsOptions>>
-> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
+> = ComponentOptionsBase<Props, RawBindings, D, C, M, Mixin, Extends, E, EE> & {
   props: PropsOptions
-} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
+} & ThisType<
+    CreateComponentPublicInstance<
+      Props,
+      RawBindings,
+      D,
+      C,
+      M,
+      Mixin,
+      Extends,
+      E
+    >
+  >
 
 export type ComponentOptions =
   | ComponentOptionsWithoutProps<any, any, any, any, any>
   | ComponentOptionsWithObjectProps<any, any, any, any, any>
   | ComponentOptionsWithArrayProps<any, any, any, any, any>
 
+export type ComponentOptionsMixin = ComponentOptionsBase<
+  any,
+  any,
+  any,
+  any,
+  any,
+  any,
+  any,
+  any,
+  any
+>
+
 export type ComputedOptions = Record<
   string,
   ComputedGetter<any> | WritableComputedOptions<any>
@@ -222,7 +277,9 @@ interface LegacyOptions<
   Props,
   D,
   C extends ComputedOptions,
-  M extends MethodOptions
+  M extends MethodOptions,
+  Mixin extends ComponentOptionsMixin,
+  Extends extends ComponentOptionsMixin
 > {
   // allow any custom options
   [key: string]: any
@@ -232,8 +289,8 @@ interface LegacyOptions<
   // since that leads to some sort of circular inference and breaks ThisType
   // for the entire component.
   data?: (
-    this: ComponentPublicInstance<Props>,
-    vm: ComponentPublicInstance<Props>
+    this: CreateComponentPublicInstance<Props>,
+    vm: CreateComponentPublicInstance<Props>
   ) => D
   computed?: C
   methods?: M
@@ -242,8 +299,8 @@ interface LegacyOptions<
   inject?: ComponentInjectOptions
 
   // composition
-  mixins?: ComponentOptions[]
-  extends?: ComponentOptions
+  mixins?: Mixin[]
+  extends?: Extends
 
   // lifecycle
   beforeCreate?(): void
@@ -261,6 +318,22 @@ interface LegacyOptions<
   errorCaptured?: ErrorCapturedHook
 }
 
+export type OptionTypesKeys = 'P' | 'B' | 'D' | 'C' | 'M'
+
+export type OptionTypesType<
+  P = {},
+  B = {},
+  D = {},
+  C extends ComputedOptions = {},
+  M extends MethodOptions = {}
+> = {
+  P: P
+  B: B
+  D: D
+  C: C
+  M: M
+}
+
 const enum OptionTypes {
   PROPS = 'Props',
   DATA = 'Data',
index c300e431b516551bbd78429a5db93ea6b89e19cd..472d4dbaf0a6054af91c857d2656d5aff5a27447 100644 (file)
@@ -67,10 +67,12 @@ type OptionalKeys<T, MakeDefaultRequired> = Exclude<
 type InferPropType<T> = T extends null
   ? any // null & true would fail to infer
   : T extends { type: null | true }
-    ? any // somehow `ObjectConstructor` when inferred from { (): T } becomes `any`
+    ? 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 }
-      : T extends Prop<infer V> ? V : T
+      : T extends BooleanConstructor | { type: BooleanConstructor }
+        ? boolean
+        : T extends Prop<infer V> ? V : T
 
 export type ExtractPropTypes<
   O,
index ff895a619690a043bb95777364446e3dd6b8a219..01c31897546fd482031baf0bcabade1923f143a6 100644 (file)
@@ -14,6 +14,9 @@ import {
   ComponentOptionsBase,
   ComputedOptions,
   MethodOptions,
+  ComponentOptionsMixin,
+  OptionTypesType,
+  OptionTypesKeys,
   resolveMergedOptions
 } from './componentOptions'
 import { normalizePropsOptions } from './componentProps'
@@ -24,6 +27,7 @@ import {
   markAttrsAccessed
 } from './componentRenderUtils'
 import { warn } from './warning'
+import { UnionToIntersection } from './helpers/typeUtils'
 
 /**
  * Custom properties added to component instances in any way and can be accessed through `this`
@@ -52,6 +56,69 @@ import { warn } from './warning'
  */
 export interface ComponentCustomProperties {}
 
+type IsDefaultMixinComponent<T> = T extends ComponentOptionsMixin
+  ? ComponentOptionsMixin extends T ? true : false
+  : false
+
+type MixinToOptionTypes<T> = T extends ComponentOptionsBase<
+  infer P,
+  infer B,
+  infer D,
+  infer C,
+  infer M,
+  infer Mixin,
+  infer Extends,
+  any
+>
+  ? OptionTypesType<P & {}, B & {}, D & {}, C & {}, M & {}> &
+      IntersectionMixin<Mixin> &
+      IntersectionMixin<Extends>
+  : never
+
+// ExtractMixin(map type) is used to resolve circularly references
+type ExtractMixin<T> = {
+  Mixin: MixinToOptionTypes<T>
+}[T extends ComponentOptionsMixin ? 'Mixin' : never]
+
+type IntersectionMixin<T> = IsDefaultMixinComponent<T> extends true
+  ? OptionTypesType<{}, {}, {}, {}, {}>
+  : UnionToIntersection<ExtractMixin<T>>
+
+type UnwrapMixinsType<
+  T,
+  Type extends OptionTypesKeys
+> = T extends OptionTypesType ? T[Type] : never
+
+type EnsureNonVoid<T> = T extends void ? {} : T
+
+export type CreateComponentPublicInstance<
+  P = {},
+  B = {},
+  D = {},
+  C extends ComputedOptions = {},
+  M extends MethodOptions = {},
+  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
+  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
+  E extends EmitsOptions = {},
+  PublicProps = P,
+  PublicMixin = IntersectionMixin<Mixin> & IntersectionMixin<Extends>,
+  PublicP = UnwrapMixinsType<PublicMixin, 'P'> & EnsureNonVoid<P>,
+  PublicB = UnwrapMixinsType<PublicMixin, 'B'> & EnsureNonVoid<B>,
+  PublicD = UnwrapMixinsType<PublicMixin, 'D'> & EnsureNonVoid<D>,
+  PublicC extends ComputedOptions = UnwrapMixinsType<PublicMixin, 'C'> &
+    EnsureNonVoid<C>,
+  PublicM extends MethodOptions = UnwrapMixinsType<PublicMixin, 'M'> &
+    EnsureNonVoid<M>
+> = ComponentPublicInstance<
+  PublicP,
+  PublicB,
+  PublicD,
+  PublicC,
+  PublicM,
+  E,
+  PublicProps,
+  ComponentOptionsBase<P, B, D, C, M, Mixin, Extends, E>
+>
 // public properties exposed on the proxy, which is used as the render context
 // in templates (as `this` in the render option)
 export type ComponentPublicInstance<
@@ -61,11 +128,12 @@ export type ComponentPublicInstance<
   C extends ComputedOptions = {},
   M extends MethodOptions = {},
   E extends EmitsOptions = {},
-  PublicProps = P
+  PublicProps = P,
+  Options = ComponentOptionsBase<any, any, any, any, any, any, any, any>
 > = {
   $: ComponentInternalInstance
   $data: D
-  $props: PublicProps
+  $props: P & PublicProps
   $attrs: Data
   $refs: Data
   $slots: Slots
@@ -73,7 +141,7 @@ export type ComponentPublicInstance<
   $parent: ComponentPublicInstance | null
   $emit: EmitFn<E>
   $el: any
-  $options: ComponentOptionsBase<P, B, D, C, M, E>
+  $options: Options
   $forceUpdate: ReactiveEffect
   $nextTick: typeof nextTick
   $watch: typeof instanceWatch
@@ -84,6 +152,12 @@ export type ComponentPublicInstance<
   M &
   ComponentCustomProperties
 
+export type ComponentPublicInstanceConstructor<
+  T extends ComponentPublicInstance
+> = {
+  new (): T
+}
+
 const publicPropertiesMap: Record<
   string,
   (i: ComponentInternalInstance) => any
diff --git a/packages/runtime-core/src/helpers/typeUtils.ts b/packages/runtime-core/src/helpers/typeUtils.ts
new file mode 100644 (file)
index 0000000..64bf512
--- /dev/null
@@ -0,0 +1,5 @@
+export type UnionToIntersection<U> = (U extends any
+  ? (k: U) => void
+  : never) extends ((k: infer I) => void)
+  ? I
+  : never
index 542634e0f4af3cb5ebf65ad44e04c5b9bc4c89aa..bc1e5f98e54ebd74c782f16816a3874eb7b8e869 100644 (file)
@@ -23,6 +23,7 @@ describe('with object props', () => {
     ddd: string[]
     eee: () => { a: string }
     fff: (a: number, b: string) => { a: boolean }
+    hhh: boolean
   }
 
   type GT = string & { __brand: unknown }
@@ -67,6 +68,10 @@ describe('with object props', () => {
       fff: {
         type: Function as PropType<(a: number, b: string) => { a: boolean }>,
         required: true
+      },
+      hhh: {
+        type: Boolean,
+        required: true
       }
     },
     setup(props) {
@@ -83,6 +88,7 @@ describe('with object props', () => {
       expectType<ExpectedProps['ddd']>(props.ddd)
       expectType<ExpectedProps['eee']>(props.eee)
       expectType<ExpectedProps['fff']>(props.fff)
+      expectType<ExpectedProps['hhh']>(props.hhh)
 
       // @ts-expect-error props should be readonly
       expectError((props.a = 1))
@@ -112,6 +118,7 @@ describe('with object props', () => {
       expectType<ExpectedProps['ddd']>(props.ddd)
       expectType<ExpectedProps['eee']>(props.eee)
       expectType<ExpectedProps['fff']>(props.fff)
+      expectType<ExpectedProps['hhh']>(props.hhh)
 
       // @ts-expect-error props should be readonly
       expectError((props.a = 1))
@@ -129,6 +136,7 @@ describe('with object props', () => {
       expectType<ExpectedProps['ddd']>(this.ddd)
       expectType<ExpectedProps['eee']>(this.eee)
       expectType<ExpectedProps['fff']>(this.fff)
+      expectType<ExpectedProps['hhh']>(this.hhh)
 
       // @ts-expect-error props on `this` should be readonly
       expectError((this.a = 1))
@@ -159,6 +167,7 @@ describe('with object props', () => {
       ddd={['ddd']}
       eee={() => ({ a: 'eee' })}
       fff={(a, b) => ({ a: a > +b })}
+      hhh={false}
       // should allow extraneous as attrs
       class="bar"
       // should allow key
@@ -210,7 +219,7 @@ describe('with object props', () => {
 // })
 
 describe('type inference w/ array props declaration', () => {
-  defineComponent({
+  const MyComponent = defineComponent({
     props: ['a', 'b'],
     setup(props) {
       // @ts-expect-error props should be readonly
@@ -231,6 +240,9 @@ describe('type inference w/ array props declaration', () => {
       expectType<number>(this.c)
     }
   })
+  expectType<JSX.Element>(<MyComponent a={[1, 2]} b="b" />)
+  // @ts-expect-error
+  expectError(<MyComponent other="other" />)
 })
 
 describe('type inference w/ options API', () => {
@@ -296,6 +308,275 @@ describe('type inference w/ options API', () => {
   })
 })
 
+describe('with mixins', () => {
+  const MixinA = defineComponent({
+    props: {
+      aP1: {
+        type: String,
+        default: 'aP1'
+      },
+      aP2: Boolean
+    },
+    data() {
+      return {
+        a: 1
+      }
+    }
+  })
+  const MixinB = defineComponent({
+    props: ['bP1', 'bP2'],
+    data() {
+      return {
+        b: 2
+      }
+    }
+  })
+  const MixinC = defineComponent({
+    data() {
+      return {
+        c: 3
+      }
+    }
+  })
+  const MixinD = defineComponent({
+    mixins: [MixinA],
+    data() {
+      return {
+        d: 4
+      }
+    },
+    computed: {
+      dC1(): number {
+        return this.d + this.a
+      },
+      dC2(): string {
+        return this.aP1 + 'dC2'
+      }
+    }
+  })
+  const MyComponent = defineComponent({
+    mixins: [MixinA, MixinB, MixinC, MixinD],
+    props: {
+      // required should make property non-void
+      z: {
+        type: String,
+        required: true
+      }
+    },
+    render() {
+      const props = this.$props
+      // props
+      expectType<string>(props.aP1)
+      expectType<boolean | undefined>(props.aP2)
+      expectType<any>(props.bP1)
+      expectType<any>(props.bP2)
+      expectType<string>(props.z)
+
+      const data = this.$data
+      expectType<number>(data.a)
+      expectType<number>(data.b)
+      expectType<number>(data.c)
+      expectType<number>(data.d)
+
+      // should also expose declared props on `this`
+      expectType<number>(this.a)
+      expectType<string>(this.aP1)
+      expectType<boolean | undefined>(this.aP2)
+      expectType<number>(this.b)
+      expectType<any>(this.bP1)
+      expectType<number>(this.c)
+      expectType<number>(this.d)
+      expectType<number>(this.dC1)
+      expectType<string>(this.dC2)
+
+      // props should be readonly
+      // @ts-expect-error
+      expectError((this.aP1 = 'new'))
+      // @ts-expect-error
+      expectError((this.z = 1))
+
+      // props on `this` should be readonly
+      // @ts-expect-error
+      expectError((this.bP1 = 1))
+
+      // string value can not assigned to number type value
+      // @ts-expect-error
+      expectError((this.c = '1'))
+
+      // setup context properties should be mutable
+      this.d = 5
+
+      return null
+    }
+  })
+
+  // Test TSX
+  expectType<JSX.Element>(
+    <MyComponent aP1={'aP'} aP2 bP1={1} bP2={[1, 2]} z={'z'} />
+  )
+
+  // missing required props
+  // @ts-expect-error
+  expectError(<MyComponent />)
+
+  // wrong prop types
+  // @ts-expect-error
+  expectError(<MyComponent aP1="ap" aP2={'wrong type'} bP1="b" z={'z'} />)
+  // @ts-expect-error
+  expectError(<MyComponent aP1={1} bP2={[1]} />)
+})
+
+describe('with extends', () => {
+  const Base = defineComponent({
+    props: {
+      aP1: Boolean,
+      aP2: {
+        type: Number,
+        default: 2
+      }
+    },
+    data() {
+      return {
+        a: 1
+      }
+    },
+    computed: {
+      c(): number {
+        return this.aP2 + this.a
+      }
+    }
+  })
+  const MyComponent = defineComponent({
+    extends: Base,
+    props: {
+      // required should make property non-void
+      z: {
+        type: String,
+        required: true
+      }
+    },
+    render() {
+      const props = this.$props
+      // props
+      expectType<boolean | undefined>(props.aP1)
+      expectType<number>(props.aP2)
+      expectType<string>(props.z)
+
+      const data = this.$data
+      expectType<number>(data.a)
+
+      // should also expose declared props on `this`
+      expectType<number>(this.a)
+      expectType<boolean | undefined>(this.aP1)
+      expectType<number>(this.aP2)
+
+      // setup context properties should be mutable
+      this.a = 5
+
+      return null
+    }
+  })
+
+  // Test TSX
+  expectType<JSX.Element>(<MyComponent aP2={3} aP1 z={'z'} />)
+
+  // missing required props
+  // @ts-expect-error
+  expectError(<MyComponent />)
+
+  // wrong prop types
+  // @ts-expect-error
+  expectError(<MyComponent aP2={'wrong type'} z={'z'} />)
+  // @ts-expect-error
+  expectError(<MyComponent aP1={3} />)
+})
+
+describe('extends with mixins', () => {
+  const Mixin = defineComponent({
+    props: {
+      mP1: {
+        type: String,
+        default: 'mP1'
+      },
+      mP2: Boolean
+    },
+    data() {
+      return {
+        a: 1
+      }
+    }
+  })
+  const Base = defineComponent({
+    props: {
+      p1: Boolean,
+      p2: {
+        type: Number,
+        default: 2
+      }
+    },
+    data() {
+      return {
+        b: 2
+      }
+    },
+    computed: {
+      c(): number {
+        return this.p2 + this.b
+      }
+    }
+  })
+  const MyComponent = defineComponent({
+    extends: Base,
+    mixins: [Mixin],
+    props: {
+      // required should make property non-void
+      z: {
+        type: String,
+        required: true
+      }
+    },
+    render() {
+      const props = this.$props
+      // props
+      expectType<boolean | undefined>(props.p1)
+      expectType<number>(props.p2)
+      expectType<string>(props.z)
+      expectType<string>(props.mP1)
+      expectType<boolean | undefined>(props.mP2)
+
+      const data = this.$data
+      expectType<number>(data.a)
+      expectType<number>(data.b)
+
+      // should also expose declared props on `this`
+      expectType<number>(this.a)
+      expectType<number>(this.b)
+      expectType<boolean | undefined>(this.p1)
+      expectType<number>(this.p2)
+      expectType<string>(this.mP1)
+      expectType<boolean | undefined>(this.mP2)
+
+      // setup context properties should be mutable
+      this.a = 5
+
+      return null
+    }
+  })
+
+  // Test TSX
+  expectType<JSX.Element>(<MyComponent mP1="p1" mP2 p1 p2={1} z={'z'} />)
+
+  // missing required props
+  // @ts-expect-error
+  expectError(<MyComponent />)
+
+  // wrong prop types
+  // @ts-expect-error
+  expectError(<MyComponent p2={'wrong type'} z={'z'} />)
+  // @ts-expect-error
+  expectError(<MyComponent mP1={3} />)
+})
+
 describe('compatibility w/ createApp', () => {
   const comp = defineComponent({})
   createApp(comp).mount('#hello')