-
Notifications
You must be signed in to change notification settings - Fork 272
feat: improve mount typings when using object syntax and infer Data type #110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f7761fc
e17f431
319aa34
e4add67
f73633e
3caf178
593c6d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,10 @@ import { | |
ComponentOptionsWithoutProps, | ||
ExtractPropTypes, | ||
Component, | ||
WritableComputedOptions, | ||
SetupContext, | ||
RenderFunction, | ||
ComponentPropsOptions, | ||
AppConfig, | ||
VNodeProps | ||
} from 'vue' | ||
|
@@ -37,8 +41,8 @@ type SlotDictionary = { | |
[key: string]: Slot | ||
} | ||
|
||
interface MountingOptions<Props> { | ||
data?: () => Record<string, unknown> | ||
interface MountingOptions<Props, Data = {}> { | ||
data?: () => Data extends object ? Partial<Data> : never | ||
props?: Props | ||
attrs?: Record<string, unknown> | ||
slots?: SlotDictionary & { | ||
|
@@ -49,40 +53,116 @@ interface MountingOptions<Props> { | |
shallow?: boolean | ||
} | ||
|
||
// TODO improve the typings of the overloads | ||
|
||
type ExtractComponent<T> = T extends { new (): infer PublicInstance } | ||
? PublicInstance | ||
: any | ||
export type ComputedOptions = Record< | ||
string, | ||
((ctx?: any) => any) | WritableComputedOptions<any> | ||
> | ||
export type ObjectEmitsOptions = Record< | ||
string, | ||
((...args: any[]) => any) | null | ||
> | ||
export type EmitsOptions = ObjectEmitsOptions | string[] | ||
|
||
// Functional component | ||
export function mount<TestedComponent extends FunctionalComponent>( | ||
export function mount< | ||
TestedComponent extends FunctionalComponent<Props>, | ||
Props | ||
>( | ||
originalComponent: TestedComponent, | ||
options?: MountingOptions<any> | ||
): VueWrapper<ComponentPublicInstance> | ||
options?: MountingOptions<Props> | ||
): VueWrapper<ComponentPublicInstance<Props>> | ||
|
||
// Component declared with defineComponent | ||
export function mount<TestedComponent extends ComponentPublicInstance>( | ||
originalComponent: { new (): TestedComponent } & Component, | ||
options?: MountingOptions<TestedComponent['$props']> | ||
options?: MountingOptions<TestedComponent['$props'], TestedComponent['$data']> | ||
): VueWrapper<TestedComponent> | ||
// Component declared with { props: { ... } } | ||
export function mount<TestedComponent extends ComponentOptionsWithObjectProps>( | ||
originalComponent: TestedComponent, | ||
options?: MountingOptions<ExtractPropTypes<TestedComponent['props'], false>> | ||
): VueWrapper<ExtractComponent<TestedComponent>> | ||
// Component declared with { props: [] } | ||
export function mount<TestedComponent extends ComponentOptionsWithArrayProps>( | ||
originalComponent: TestedComponent, | ||
options?: MountingOptions<Record<string, any>> | ||
): VueWrapper<ExtractComponent<TestedComponent>> | ||
|
||
// Component declared with no props | ||
export function mount< | ||
TestedComponent extends ComponentOptionsWithoutProps, | ||
ComponentT extends ComponentOptionsWithoutProps & {} | ||
Props = {}, | ||
RawBindings = {}, | ||
D = {}, | ||
C extends ComputedOptions = {}, | ||
M extends Record<string, Function> = {}, | ||
E extends EmitsOptions = Record<string, any>, | ||
EE extends string = string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here: if we really need all the generics, it would be great to have a test showcasing why. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is basically a To make it simpler to maintain (bring the types from that file), I can add a comment explaining. |
||
>( | ||
componentOptions: ComponentOptionsWithoutProps< | ||
Props, | ||
RawBindings, | ||
D, | ||
C, | ||
M, | ||
E, | ||
EE | ||
>, | ||
options?: MountingOptions<never, D> | ||
): VueWrapper< | ||
ComponentPublicInstance<Props, RawBindings, D, C, M, E, VNodeProps & Props> | ||
> | ||
|
||
// Component declared with { props: [] } | ||
export function mount< | ||
PropNames extends string, | ||
RawBindings, | ||
D, | ||
C extends ComputedOptions = {}, | ||
M extends Record<string, Function> = {}, | ||
E extends EmitsOptions = Record<string, any>, | ||
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<Props, D> | ||
): VueWrapper<ComponentPublicInstance<Props, RawBindings, D, C, M, E>> | ||
|
||
// 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<ComponentPropsOptions>, | ||
RawBindings, | ||
D, | ||
C extends ComputedOptions = {}, | ||
M extends Record<string, Function> = {}, | ||
E extends EmitsOptions = Record<string, any>, | ||
EE extends string = string | ||
>( | ||
originalComponent: ComponentT extends { new (): any } ? never : ComponentT, | ||
options?: MountingOptions<never> | ||
): VueWrapper<ExtractComponent<TestedComponent>> | ||
componentOptions: ComponentOptionsWithObjectProps< | ||
PropsOptions, | ||
RawBindings, | ||
D, | ||
C, | ||
M, | ||
E, | ||
EE | ||
>, | ||
options?: MountingOptions<ExtractPropTypes<PropsOptions>, D> | ||
): VueWrapper< | ||
ComponentPublicInstance< | ||
ExtractPropTypes<PropsOptions>, | ||
RawBindings, | ||
D, | ||
C, | ||
M, | ||
E, | ||
VNodeProps & ExtractPropTypes<PropsOptions, false> | ||
> | ||
> | ||
|
||
// implementation | ||
pikax marked this conversation as resolved.
Show resolved
Hide resolved
|
||
export function mount( | ||
originalComponent: any, | ||
options?: MountingOptions<any> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,12 +13,23 @@ const AppWithDefine = defineComponent({ | |
template: '' | ||
}) | ||
|
||
// accept props | ||
let wrapper = mount(AppWithDefine, { | ||
props: { a: 'Hello', b: 2 } | ||
}) | ||
// vm is properly typed | ||
expectType<string>(wrapper.vm.a) | ||
// accept props- vm is properly typed | ||
expectType<string>( | ||
mount(AppWithDefine, { | ||
props: { a: 'Hello', b: 2 } | ||
}).vm.a | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I think the test is more readable when splitted like it was. Same for the following changes. |
||
|
||
// no data provided | ||
expectError( | ||
mount(AppWithDefine, { | ||
data() { | ||
return { | ||
myVal: 1 | ||
} | ||
} | ||
}) | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 I think more tests with data would be awesome. Can we pass |
||
|
||
// can receive extra props | ||
// ideally, it should not | ||
|
@@ -45,12 +56,12 @@ const AppWithProps = { | |
template: '' | ||
} | ||
|
||
// accept props | ||
wrapper = mount(AppWithProps, { | ||
props: { a: 'Hello' } | ||
}) | ||
// vm is properly typed | ||
expectType<string>(wrapper.vm.a) | ||
// accept props - vm is properly typed | ||
expectType<string>( | ||
mount(AppWithProps, { | ||
props: { a: 'Hello' } | ||
}).vm.a | ||
) | ||
|
||
// can't receive extra props | ||
expectError( | ||
|
@@ -71,31 +82,65 @@ const AppWithArrayProps = { | |
template: '' | ||
} | ||
|
||
// accept props | ||
wrapper = mount(AppWithArrayProps, { | ||
props: { a: 'Hello' } | ||
}) | ||
// vm is properly typed | ||
expectType<string>(wrapper.vm.a) | ||
// accept props - vm is properly typed | ||
expectType<string>( | ||
mount(AppWithArrayProps, { | ||
props: { a: 'Hello' } | ||
}).vm.a | ||
) | ||
|
||
// can receive extra props | ||
// as they are declared as `string[]` | ||
mount(AppWithArrayProps, { | ||
props: { a: 'Hello', b: 2 } | ||
}) | ||
expectType<number>( | ||
mount(AppWithArrayProps, { | ||
props: { a: 'Hello', b: 2 } | ||
}).vm.b | ||
) | ||
|
||
// cannot receive extra props | ||
// if they pass use object inside | ||
expectError( | ||
mount( | ||
{ | ||
props: ['a'] | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm happy to see that it throws an error, but I don't get how this is different from the test case with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Basically when you use mount({
props: ['a']
})
// type
type MountTyped = {
props: ['a']
}
const Comp = {
props: ['a']
}
//type
type CompTyped = {
props: string[]
} Basically using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it! So the previous test should also throw if the props were declared as const right? const AppWithArrayProps = {
props: ['a'] as const,
template: ''
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That doesn't seem to happen, if you define |
||
{ | ||
props: { | ||
b: 2 | ||
} | ||
} | ||
) | ||
) | ||
|
||
const AppWithoutProps = { | ||
template: '' | ||
} | ||
|
||
// can't receive extra props | ||
expectError( | ||
(wrapper = mount(AppWithoutProps, { | ||
mount(AppWithoutProps, { | ||
props: { b: 'Hello' } | ||
})) | ||
}) | ||
) | ||
|
||
// except if explicitly cast | ||
mount(AppWithoutProps, { | ||
props: { b: 'Hello' } as never | ||
}) | ||
|
||
// Functional tests | ||
|
||
// wrong props | ||
expectError((props: { a: 1 }) => {}, { | ||
props: { | ||
a: '222' | ||
} | ||
}) | ||
|
||
expectType<number>( | ||
mount((props: { a: 1 }, ctx) => {}, { | ||
props: { | ||
a: 22 | ||
} | ||
}).vm.a | ||
) |
Uh oh!
There was an error while loading. Please reload this page.