From f7761fcec82274191d3759ee15ec077977ec8216 Mon Sep 17 00:00:00 2001 From: pikax Date: Tue, 5 May 2020 11:14:25 +0100 Subject: [PATCH 1/6] feat: improve mount typings when using object syntax and infer Data type --- src/mount.ts | 145 ++++++++++++++++++++++++++++++++------- test-dts/mount.d-test.ts | 51 +++++++++----- 2 files changed, 154 insertions(+), 42 deletions(-) diff --git a/src/mount.ts b/src/mount.ts index ba0467f41..30feed556 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -11,7 +11,12 @@ import { ComponentOptionsWithArrayProps, ComponentOptionsWithoutProps, ExtractPropTypes, - Component + Component, + VNodeProps, + WritableComputedOptions, + SetupContext, + RenderFunction, + ComponentPropsOptions } from 'vue' import { config } from './config' @@ -29,8 +34,8 @@ import { stubComponents } from './stubs' type Slot = VNode | string | { render: Function } -interface MountingOptions { - data?: () => Record +interface MountingOptions { + data?: () => Data props?: Props attrs?: Record slots?: { @@ -42,35 +47,127 @@ interface MountingOptions { shallow?: boolean } -// TODO improve the typings of the overloads - -type ExtractComponent = T extends { new (): infer PublicInstance } - ? PublicInstance - : any +export type ComputedOptions = Record< + string, + ((ctx?: any) => any) | WritableComputedOptions +> +export type ObjectEmitsOptions = Record< + string, + ((...args: any[]) => any) | null +> +export type EmitsOptions = ObjectEmitsOptions | string[] // Component declared with defineComponent export function mount( originalComponent: { new (): TestedComponent } & Component, - options?: MountingOptions + options?: MountingOptions ): VueWrapper -// Component declared with { props: { ... } } -export function mount( - originalComponent: TestedComponent, - options?: MountingOptions> -): VueWrapper> -// Component declared with { props: [] } -export function mount( - originalComponent: TestedComponent, - options?: MountingOptions> -): VueWrapper> + +// Functional Component declared with (props, ctx)=> render +export function mount( + setup: ( + props: Readonly, + ctx: SetupContext + ) => RawBindings | RenderFunction, + + options: MountingOptions +): VueWrapper< + ComponentPublicInstance< + Props, + RawBindings, + {}, + {}, + {}, + // public props + VNodeProps & Props + > +> + // Component declared with no props export function mount< - TestedComponent extends ComponentOptionsWithoutProps, - ComponentT extends ComponentOptionsWithoutProps & {} + Props = {}, + RawBindings = {}, + D = {}, + C extends ComputedOptions = {}, + M extends Record = {}, + E extends EmitsOptions = Record, + EE extends string = string >( - originalComponent: ComponentT extends { new (): any } ? never : ComponentT, - options?: MountingOptions -): VueWrapper> + componentOptions: ComponentOptionsWithoutProps< + Props, + RawBindings, + D, + C, + M, + E, + EE + >, + options?: MountingOptions +): VueWrapper< + ComponentPublicInstance +> + +// Component declared with { props: [] } +export function mount< + PropNames extends string, + RawBindings, + D, + C extends ComputedOptions = {}, + M extends Record = {}, + E extends EmitsOptions = Record, + EE extends string = string, + Props extends Readonly<{ [key in PropNames]?: any }> = Readonly< + { [key in PropNames]?: any } + > +>( + componentOptions: ComponentOptionsWithArrayProps< + PropNames, + RawBindings, + D, + C, + M, + E, + EE, + Props + >, + options?: MountingOptions +): VueWrapper> + +// Component declared with { props: { ... } } +export function mount< + // the Readonly constraint allows TS to treat the type of { required: true } + // as constant instead of boolean. + PropsOptions extends Readonly, + RawBindings, + D, + C extends ComputedOptions = {}, + M extends Record = {}, + E extends EmitsOptions = Record, + EE extends string = string +>( + componentOptions: ComponentOptionsWithObjectProps< + PropsOptions, + RawBindings, + D, + C, + M, + E, + EE + >, + options?: MountingOptions, D> +): VueWrapper< + ComponentPublicInstance< + ExtractPropTypes, + RawBindings, + D, + C, + M, + E, + VNodeProps & ExtractPropTypes + > +> + +// implementation export function mount( originalComponent: any, options?: MountingOptions diff --git a/test-dts/mount.d-test.ts b/test-dts/mount.d-test.ts index eda62aace..cea36a648 100644 --- a/test-dts/mount.d-test.ts +++ b/test-dts/mount.d-test.ts @@ -13,12 +13,12 @@ const AppWithDefine = defineComponent({ template: '' }) -// accept props -let wrapper = mount(AppWithDefine, { - props: { a: 'Hello', b: 2 } -}) -// vm is properly typed -expectType(wrapper.vm.a) +// accept props- vm is properly typed +expectType( + mount(AppWithDefine, { + props: { a: 'Hello', b: 2 } + }).vm.a +) // can receive extra props // ideally, it should not @@ -45,12 +45,12 @@ const AppWithProps = { template: '' } -// accept props -wrapper = mount(AppWithProps, { - props: { a: 'Hello' } -}) -// vm is properly typed -expectType(wrapper.vm.a) +// accept props - vm is properly typed +expectType( + mount(AppWithProps, { + props: { a: 'Hello' } + }).vm.a +) // can't receive extra props expectError( @@ -71,12 +71,12 @@ const AppWithArrayProps = { template: '' } -// accept props -wrapper = mount(AppWithArrayProps, { - props: { a: 'Hello' } -}) -// vm is properly typed -expectType(wrapper.vm.a) +// accept props - vm is properly typed +expectType( + mount(AppWithArrayProps, { + props: { a: 'Hello' } + }).vm.a +) // can receive extra props // as they are declared as `string[]` @@ -84,6 +84,21 @@ mount(AppWithArrayProps, { props: { a: 'Hello', b: 2 } }) +// cannot receive extra props +// if they pass use object inside +expectError( + mount( + { + props: ['a'] + }, + { + props: { + b: 2 + } + } + ) +) + const AppWithoutProps = { template: '' } From e17f431a34ede4bf53934b1f6ef7ecde16f819eb Mon Sep 17 00:00:00 2001 From: pikax Date: Tue, 5 May 2020 11:20:01 +0100 Subject: [PATCH 2/6] prevent Data to be unkown --- src/mount.ts | 4 ++-- test-dts/mount.d-test.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/mount.ts b/src/mount.ts index 30feed556..31d62856f 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -34,8 +34,8 @@ import { stubComponents } from './stubs' type Slot = VNode | string | { render: Function } -interface MountingOptions { - data?: () => Data +interface MountingOptions { + data?: () => Data extends unknown ? never : Data props?: Props attrs?: Record slots?: { diff --git a/test-dts/mount.d-test.ts b/test-dts/mount.d-test.ts index cea36a648..295c81eab 100644 --- a/test-dts/mount.d-test.ts +++ b/test-dts/mount.d-test.ts @@ -20,6 +20,17 @@ expectType( }).vm.a ) +// no data provided +expectError( + mount(AppWithDefine, { + data() { + return { + myVal: 1 + } + } + }) +) + // can receive extra props // ideally, it should not // but the props have type { a: string } & VNodeProps From 319aa3473b017166b6da9948c12e5b44233562f0 Mon Sep 17 00:00:00 2001 From: pikax Date: Tue, 5 May 2020 11:23:14 +0100 Subject: [PATCH 3/6] remove `extends object` and fix props --- src/mount.ts | 4 ++-- test-dts/mount.d-test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mount.ts b/src/mount.ts index 31d62856f..b0e568262 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -34,7 +34,7 @@ import { stubComponents } from './stubs' type Slot = VNode | string | { render: Function } -interface MountingOptions { +interface MountingOptions { data?: () => Data extends unknown ? never : Data props?: Props attrs?: Record @@ -102,7 +102,7 @@ export function mount< E, EE >, - options?: MountingOptions + options?: MountingOptions ): VueWrapper< ComponentPublicInstance > diff --git a/test-dts/mount.d-test.ts b/test-dts/mount.d-test.ts index 295c81eab..b84753926 100644 --- a/test-dts/mount.d-test.ts +++ b/test-dts/mount.d-test.ts @@ -116,9 +116,9 @@ const AppWithoutProps = { // can't receive extra props expectError( - (wrapper = mount(AppWithoutProps, { + mount(AppWithoutProps, { props: { b: 'Hello' } - })) + }) ) // except if explicitly cast From e4add672019fb98af470fa561af484e1202e88d5 Mon Sep 17 00:00:00 2001 From: pikax Date: Tue, 5 May 2020 11:40:51 +0100 Subject: [PATCH 4/6] allow partial Data --- src/mount.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mount.ts b/src/mount.ts index b0e568262..2976f82e0 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -35,7 +35,7 @@ import { stubComponents } from './stubs' type Slot = VNode | string | { render: Function } interface MountingOptions { - data?: () => Data extends unknown ? never : Data + data?: () => unknown extends Data ? never : Partial props?: Props attrs?: Record slots?: { From f73633e0b2c56a23afabbd5e43e183f11d448b39 Mon Sep 17 00:00:00 2001 From: pikax Date: Tue, 5 May 2020 15:03:35 +0100 Subject: [PATCH 5/6] Data extending object --- src/mount.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mount.ts b/src/mount.ts index 2976f82e0..0b5b9d6c4 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -35,7 +35,7 @@ import { stubComponents } from './stubs' type Slot = VNode | string | { render: Function } interface MountingOptions { - data?: () => unknown extends Data ? never : Partial + data?: () => Data extends object ? Partial : never props?: Props attrs?: Record slots?: { From 593c6d0fd28ef4419d49b5724cb75a4eee7c94d2 Mon Sep 17 00:00:00 2001 From: pikax Date: Mon, 25 May 2020 10:51:39 +0100 Subject: [PATCH 6/6] improve mount of functionalComponent and add couple more tests --- src/mount.ts | 30 +++++++----------------------- test-dts/mount.d-test.ts | 25 ++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/mount.ts b/src/mount.ts index 8ea94b091..2d83869b9 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -64,36 +64,20 @@ export type ObjectEmitsOptions = Record< export type EmitsOptions = ObjectEmitsOptions | string[] // Functional component -export function mount( +export function mount< + TestedComponent extends FunctionalComponent, + Props +>( originalComponent: TestedComponent, - options?: MountingOptions -): VueWrapper + options?: MountingOptions +): VueWrapper> + // Component declared with defineComponent export function mount( originalComponent: { new (): TestedComponent } & Component, options?: MountingOptions ): VueWrapper -// Functional Component declared with (props, ctx)=> render -export function mount( - setup: ( - props: Readonly, - ctx: SetupContext - ) => RawBindings | RenderFunction, - - options: MountingOptions -): VueWrapper< - ComponentPublicInstance< - Props, - RawBindings, - {}, - {}, - {}, - // public props - VNodeProps & Props - > -> - // Component declared with no props export function mount< Props = {}, diff --git a/test-dts/mount.d-test.ts b/test-dts/mount.d-test.ts index b84753926..d8274e10d 100644 --- a/test-dts/mount.d-test.ts +++ b/test-dts/mount.d-test.ts @@ -91,9 +91,11 @@ expectType( // can receive extra props // as they are declared as `string[]` -mount(AppWithArrayProps, { - props: { a: 'Hello', b: 2 } -}) +expectType( + mount(AppWithArrayProps, { + props: { a: 'Hello', b: 2 } + }).vm.b +) // cannot receive extra props // if they pass use object inside @@ -125,3 +127,20 @@ expectError( mount(AppWithoutProps, { props: { b: 'Hello' } as never }) + +// Functional tests + +// wrong props +expectError((props: { a: 1 }) => {}, { + props: { + a: '222' + } +}) + +expectType( + mount((props: { a: 1 }, ctx) => {}, { + props: { + a: 22 + } + }).vm.a +)