Skip to content

Commit 1853ec4

Browse files
authored
Merge pull request #110 from vuejs/types/better_mount_object_typing
feat: improve mount typings when using object syntax and infer Data type
2 parents bc40317 + 593c6d0 commit 1853ec4

File tree

3 files changed

+179
-53
lines changed

3 files changed

+179
-53
lines changed

src/mount.ts

Lines changed: 106 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import {
1313
ComponentOptionsWithoutProps,
1414
ExtractPropTypes,
1515
Component,
16+
WritableComputedOptions,
17+
SetupContext,
18+
RenderFunction,
19+
ComponentPropsOptions,
1620
AppConfig,
1721
VNodeProps
1822
} from 'vue'
@@ -37,8 +41,8 @@ type SlotDictionary = {
3741
[key: string]: Slot
3842
}
3943

40-
interface MountingOptions<Props> {
41-
data?: () => Record<string, unknown>
44+
interface MountingOptions<Props, Data = {}> {
45+
data?: () => Data extends object ? Partial<Data> : never
4246
props?: Props
4347
attrs?: Record<string, unknown>
4448
slots?: SlotDictionary & {
@@ -49,40 +53,116 @@ interface MountingOptions<Props> {
4953
shallow?: boolean
5054
}
5155

52-
// TODO improve the typings of the overloads
53-
54-
type ExtractComponent<T> = T extends { new (): infer PublicInstance }
55-
? PublicInstance
56-
: any
56+
export type ComputedOptions = Record<
57+
string,
58+
((ctx?: any) => any) | WritableComputedOptions<any>
59+
>
60+
export type ObjectEmitsOptions = Record<
61+
string,
62+
((...args: any[]) => any) | null
63+
>
64+
export type EmitsOptions = ObjectEmitsOptions | string[]
5765

5866
// Functional component
59-
export function mount<TestedComponent extends FunctionalComponent>(
67+
export function mount<
68+
TestedComponent extends FunctionalComponent<Props>,
69+
Props
70+
>(
6071
originalComponent: TestedComponent,
61-
options?: MountingOptions<any>
62-
): VueWrapper<ComponentPublicInstance>
72+
options?: MountingOptions<Props>
73+
): VueWrapper<ComponentPublicInstance<Props>>
74+
6375
// Component declared with defineComponent
6476
export function mount<TestedComponent extends ComponentPublicInstance>(
6577
originalComponent: { new (): TestedComponent } & Component,
66-
options?: MountingOptions<TestedComponent['$props']>
78+
options?: MountingOptions<TestedComponent['$props'], TestedComponent['$data']>
6779
): VueWrapper<TestedComponent>
68-
// Component declared with { props: { ... } }
69-
export function mount<TestedComponent extends ComponentOptionsWithObjectProps>(
70-
originalComponent: TestedComponent,
71-
options?: MountingOptions<ExtractPropTypes<TestedComponent['props'], false>>
72-
): VueWrapper<ExtractComponent<TestedComponent>>
73-
// Component declared with { props: [] }
74-
export function mount<TestedComponent extends ComponentOptionsWithArrayProps>(
75-
originalComponent: TestedComponent,
76-
options?: MountingOptions<Record<string, any>>
77-
): VueWrapper<ExtractComponent<TestedComponent>>
80+
7881
// Component declared with no props
7982
export function mount<
80-
TestedComponent extends ComponentOptionsWithoutProps,
81-
ComponentT extends ComponentOptionsWithoutProps & {}
83+
Props = {},
84+
RawBindings = {},
85+
D = {},
86+
C extends ComputedOptions = {},
87+
M extends Record<string, Function> = {},
88+
E extends EmitsOptions = Record<string, any>,
89+
EE extends string = string
90+
>(
91+
componentOptions: ComponentOptionsWithoutProps<
92+
Props,
93+
RawBindings,
94+
D,
95+
C,
96+
M,
97+
E,
98+
EE
99+
>,
100+
options?: MountingOptions<never, D>
101+
): VueWrapper<
102+
ComponentPublicInstance<Props, RawBindings, D, C, M, E, VNodeProps & Props>
103+
>
104+
105+
// Component declared with { props: [] }
106+
export function mount<
107+
PropNames extends string,
108+
RawBindings,
109+
D,
110+
C extends ComputedOptions = {},
111+
M extends Record<string, Function> = {},
112+
E extends EmitsOptions = Record<string, any>,
113+
EE extends string = string,
114+
Props extends Readonly<{ [key in PropNames]?: any }> = Readonly<
115+
{ [key in PropNames]?: any }
116+
>
117+
>(
118+
componentOptions: ComponentOptionsWithArrayProps<
119+
PropNames,
120+
RawBindings,
121+
D,
122+
C,
123+
M,
124+
E,
125+
EE,
126+
Props
127+
>,
128+
options?: MountingOptions<Props, D>
129+
): VueWrapper<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
130+
131+
// Component declared with { props: { ... } }
132+
export function mount<
133+
// the Readonly constraint allows TS to treat the type of { required: true }
134+
// as constant instead of boolean.
135+
PropsOptions extends Readonly<ComponentPropsOptions>,
136+
RawBindings,
137+
D,
138+
C extends ComputedOptions = {},
139+
M extends Record<string, Function> = {},
140+
E extends EmitsOptions = Record<string, any>,
141+
EE extends string = string
82142
>(
83-
originalComponent: ComponentT extends { new (): any } ? never : ComponentT,
84-
options?: MountingOptions<never>
85-
): VueWrapper<ExtractComponent<TestedComponent>>
143+
componentOptions: ComponentOptionsWithObjectProps<
144+
PropsOptions,
145+
RawBindings,
146+
D,
147+
C,
148+
M,
149+
E,
150+
EE
151+
>,
152+
options?: MountingOptions<ExtractPropTypes<PropsOptions>, D>
153+
): VueWrapper<
154+
ComponentPublicInstance<
155+
ExtractPropTypes<PropsOptions>,
156+
RawBindings,
157+
D,
158+
C,
159+
M,
160+
E,
161+
VNodeProps & ExtractPropTypes<PropsOptions, false>
162+
>
163+
>
164+
165+
// implementation
86166
export function mount(
87167
originalComponent: any,
88168
options?: MountingOptions<any>

test-dts/mount.d-test.ts

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,23 @@ const AppWithDefine = defineComponent({
1313
template: ''
1414
})
1515

16-
// accept props
17-
let wrapper = mount(AppWithDefine, {
18-
props: { a: 'Hello', b: 2 }
19-
})
20-
// vm is properly typed
21-
expectType<string>(wrapper.vm.a)
16+
// accept props- vm is properly typed
17+
expectType<string>(
18+
mount(AppWithDefine, {
19+
props: { a: 'Hello', b: 2 }
20+
}).vm.a
21+
)
22+
23+
// no data provided
24+
expectError(
25+
mount(AppWithDefine, {
26+
data() {
27+
return {
28+
myVal: 1
29+
}
30+
}
31+
})
32+
)
2233

2334
// can receive extra props
2435
// ideally, it should not
@@ -45,12 +56,12 @@ const AppWithProps = {
4556
template: ''
4657
}
4758

48-
// accept props
49-
wrapper = mount(AppWithProps, {
50-
props: { a: 'Hello' }
51-
})
52-
// vm is properly typed
53-
expectType<string>(wrapper.vm.a)
59+
// accept props - vm is properly typed
60+
expectType<string>(
61+
mount(AppWithProps, {
62+
props: { a: 'Hello' }
63+
}).vm.a
64+
)
5465

5566
// can't receive extra props
5667
expectError(
@@ -71,31 +82,65 @@ const AppWithArrayProps = {
7182
template: ''
7283
}
7384

74-
// accept props
75-
wrapper = mount(AppWithArrayProps, {
76-
props: { a: 'Hello' }
77-
})
78-
// vm is properly typed
79-
expectType<string>(wrapper.vm.a)
85+
// accept props - vm is properly typed
86+
expectType<string>(
87+
mount(AppWithArrayProps, {
88+
props: { a: 'Hello' }
89+
}).vm.a
90+
)
8091

8192
// can receive extra props
8293
// as they are declared as `string[]`
83-
mount(AppWithArrayProps, {
84-
props: { a: 'Hello', b: 2 }
85-
})
94+
expectType<number>(
95+
mount(AppWithArrayProps, {
96+
props: { a: 'Hello', b: 2 }
97+
}).vm.b
98+
)
99+
100+
// cannot receive extra props
101+
// if they pass use object inside
102+
expectError(
103+
mount(
104+
{
105+
props: ['a']
106+
},
107+
{
108+
props: {
109+
b: 2
110+
}
111+
}
112+
)
113+
)
86114

87115
const AppWithoutProps = {
88116
template: ''
89117
}
90118

91119
// can't receive extra props
92120
expectError(
93-
(wrapper = mount(AppWithoutProps, {
121+
mount(AppWithoutProps, {
94122
props: { b: 'Hello' }
95-
}))
123+
})
96124
)
97125

98126
// except if explicitly cast
99127
mount(AppWithoutProps, {
100128
props: { b: 'Hello' } as never
101129
})
130+
131+
// Functional tests
132+
133+
// wrong props
134+
expectError((props: { a: 1 }) => {}, {
135+
props: {
136+
a: '222'
137+
}
138+
})
139+
140+
expectType<number>(
141+
mount((props: { a: 1 }, ctx) => {}, {
142+
props: {
143+
a: 22
144+
}
145+
}).vm.a
146+
)

test-dts/shallowMount.d-test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ const AppWithProps = {
4646
}
4747

4848
// accept props
49-
wrapper = shallowMount(AppWithProps, {
50-
props: { a: 'Hello' }
51-
})
5249
// vm is properly typed
53-
expectType<string>(wrapper.vm.a)
50+
expectType<string>(
51+
shallowMount(AppWithProps, {
52+
props: { a: 'Hello' }
53+
}).vm.a
54+
)
5455

5556
// can't receive extra props
5657
expectError(

0 commit comments

Comments
 (0)