core
core copied to clipboard
types(defineComponent): Stricter Component Type + helpers
closes #7259 #9296
Breaking Changes
This PR causes breaking changes, because the types become more stricter on Component in general, this is necessary to prevent false type safety by preventing the DefineComponent and Component from being any.
Ecosystem
There's companion PRs for the ecosystem
- https://github.com/vuejs/docs/pull/2577
- https://github.com/vuejs/test-utils/pull/2242
- https://github.com/vuejs/language-tools/pull/3731
- https://github.com/vuejs/router/pull/2039
- https://github.com/vuetifyjs/vuetify/pull/18785
- https://github.com/vueuse/vueuse/pull/3574
Preliminary work as been done, once 3.5 alpha is released I'll update the PRs to add the alpha as dependency.
I expect Component libraries to be broken, please reach out to me through Discord on #typescript channel, tagging @pikax.
Details
Added the ability to extract the original options which the component was created:
const comp = defineComponent({
randomOption: 'option',
})
expectType<string>(comp.randomOption)
This will be used to build the component types helpers
Improvements
defineComponent
defineComponent has become more stricter, now is possible to extract component options as they were passed
const comp = defineComponent({ myFoo: { a: 1 } })
comp.myFoo // { a: 1 }
defineAsyncComponent
defineAsyncComponent got it's types improved now correctly exposes it's own type as options and on rendering the innerComponent.
const Comp = defineAsyncComponent(async ()=> defineComponent( { name: 'myComp'}));
Comp.name // 'AsyncComponentWrapper'
Comp.__asyncResolved // DefineComponent | undefined
defineCustomElement
defineCustomElement improvement on resolving the props for passed components, it should correctly infer props from passed components.
Helpers
There's 2 types of helpers introduced, Extract* and Component* , the Extract will extract the original type passed to define the component, the Component will provide the typescript type.
Extract can be used to enhanced the component options, eg: add more props
- ExtractComponentOptions
- ExtractComponentProp
- ExtractComponentSlots
- ExtractComponentEmits
- ComponentProps
- ComponentSlots
- ComponentEmits
ExtractComponentOptions
Extract the original options from the component.
NOTE: It does not work with functional components, because it causes the Generics information to be lost.
const options = {
props: { foo: String },
setup(){
// ...
}
}
const Comp = defineComponent(options)
expectType<ExtractComponentOptions<typeof Comp>>(options)
ExtractComponentProp
Extracts the object passed to create options or if options are not available it will provide the typescript object definition.
const options = {
props: { foo: String },
setup(){
// ...
}
}
const Comp = defineComponent(options)
expectType<ExtractComponentProp<typeof Comp>>(options.props)
// if the original options are not provided, eg: Volar or functional components
const Comp = defineComponent((props: { a: 1 }) => () => {})
expectType<ExtractComponentProp<typeof Comp>>({ a: 1 as 1 })
// @ts-expect-error not valid type
expectType<ExtractComponentProp<typeof Comp>>({ a: 2 /* the type is { a: 1 } */ })
ExtractComponentSlots
Extracts original slot options from the component, if no options are available it won't be able to infer them, since slots are not exposed on functional components, that information is lost on defineComponent
const options = {
slots: {
default(arg: { msg: string }) {}
},
setup() {
// ...
}
}
const Comp = defineComponent(options)
expectType<ExtractComponentSlots<typeof Comp>>(options.slots)
ExtractComponentEmits
Extracts original emits options from the component, if no options are available it won't be able to infer them, since emits are not exposed on functional components, that information is lost on defineComponent. You can use ComponentProps to get the runtime type, props on functional components includes on*.
const options = {
emits: {
foo: (payload: { bar: number }) => true
},
setup() {
// ...
}
}
const Comp = defineComponent(options)
expectType<ExtractComponentEmits<typeof Comp>>(options.emits)
ComponentProps
Provides the typescript representation of component props, it appends emits props by default.
const options = {
props: { foo: String },
setup() {
// ...
}
}
const Comp = defineComponent(options)
expectType<{
foo?: string | undefined
}>({} as ComponentProps<typeof Comp>)
// with emits
const optionsEmits = {
props: { modelValue: String },
emits: {
'update:modelValue': (payload: string) => true
},
setup() {
// ...
}
}
const Comp = defineComponent(optionsEmits)
expectType<{
foo?: string | undefined
'onUpdate:modelValue'?: (payload: string) => void
}>({} as ComponentProps<typeof Comp>)
// exclude emits
const a = {} as ComponentProps<typeof Comp, true>
// @ts-expect-error
a['onUpdate:modelValue']
ComponentSlots
const optionsSlots = {
slots: {} as {
test: (payload: string) => any
},
setup() {
// ...
}
}
const Comp = defineComponent(optionsSlots)
expectType<{
test: (payload: string) => any
}>({} as ComponentSlots<typeof Comp>)
ComponentEmits
DeclareComponent
DeclareComponent is an helper to create Vue components through types without needing to pass 16 arguments to the type.
It contains 5 arguments:
export type DeclareComponent<
Props extends Record<string, any> | { new (): ComponentPublicInstance } = {},
Data extends Record<string, any> = {},
Emits extends EmitsOptions = {},
Slots extends SlotsType = {},
Options extends Record<PropertyKey, any> = {},
>
Props
It allows to pass just props object or a new Component generic, if a component is passed it will short-circuit the type.
Data
Data exposed publicly when using template :ref.
Options
Allowing to Provide custom properties to the Options object.
Size Report
Bundles
| File | Size | Gzip | Brotli |
|---|---|---|---|
| runtime-dom.global.prod.js | 89.3 kB | 34 kB | 30.6 kB |
| vue.global.prod.js | 146 kB | 53.2 kB | 47.6 kB |
Usages
| Name | Size | Gzip | Brotli |
|---|---|---|---|
| createApp | 49.7 kB | 19.5 kB | 17.8 kB |
| createSSRApp | 53 kB | 20.8 kB | 19 kB |
| defineCustomElement | 52 kB | 20.2 kB | 18.4 kB |
| overall | 63.2 kB | 24.4 kB | 22.2 kB |
/ecosystem-ci run
📝 Ran ecosystem CI: Open
| suite | result | latest scheduled |
|---|---|---|
| language-tools | :x: failure | :x: failure |
| nuxt | :x: failure | :white_check_mark: success |
| pinia | :x: failure | :white_check_mark: success |
| quasar | :x: failure | :white_check_mark: success |
| router | :x: failure | :white_check_mark: success |
| test-utils | :x: failure | :x: failure |
| vant | :x: failure | :white_check_mark: success |
| vite-plugin-vue | :x: failure | :white_check_mark: success |
| vitepress | :x: failure | :white_check_mark: success |
| vue-i18n | :x: failure | :white_check_mark: success |
| vue-macros | :x: failure | :white_check_mark: success |
| vuetify | :x: failure | :white_check_mark: success |
| vueuse | :x: failure | :x: failure |
| vue-simple-compiler | :x: failure | :white_check_mark: success |
📝 Ran ecosystem CI: Open
| suite | result | latest scheduled |
|---|---|---|
| language-tools | :x: failure | :x: failure |
| nuxt | :x: failure | :white_check_mark: success |
| pinia | :white_check_mark: success | :white_check_mark: success |
| quasar | :white_check_mark: success | :white_check_mark: success |
| router | :x: failure | :white_check_mark: success |
| test-utils | :x: failure | :x: failure |
| vant | :x: failure | :white_check_mark: success |
| vite-plugin-vue | :white_check_mark: success | :white_check_mark: success |
| vitepress | :x: failure | :white_check_mark: success |
| vue-i18n | :white_check_mark: success | :white_check_mark: success |
| vue-macros | :x: failure | :white_check_mark: success |
| vuetify | :x: failure | :white_check_mark: success |
| vueuse | :x: failure | :x: failure |
| vue-simple-compiler | :white_check_mark: success | :white_check_mark: success |
/ecosystem-ci run
📝 Ran ecosystem CI: Open
| suite | result | latest scheduled |
|---|---|---|
| language-tools | :x: failure | :x: failure |
| nuxt | :x: failure | :white_check_mark: success |
| pinia | :x: failure | :white_check_mark: success |
| quasar | :white_check_mark: success | :white_check_mark: success |
| router | :x: failure | :white_check_mark: success |
| test-utils | :x: failure | :x: failure |
| vant | :white_check_mark: success | :white_check_mark: success |
| vite-plugin-vue | :white_check_mark: success | :white_check_mark: success |
| vitepress | :x: failure | :white_check_mark: success |
| vue-i18n | :white_check_mark: success | :white_check_mark: success |
| vue-macros | :x: failure | :white_check_mark: success |
| vuetify | :x: failure | :white_check_mark: success |
| vueuse | :x: failure | :white_check_mark: success |
| vue-simple-compiler | :white_check_mark: success | :white_check_mark: success |
Deploy Preview for vue-sfc-playground failed.
| Name | Link |
|---|---|
| Latest commit | 1d7ef7f6848bd08a880d76045968b3cc75b7b022 |
| Latest deploy log | https://app.netlify.com/sites/vue-sfc-playground/deploys/656203f0796cc20008f0ede8 |
/ecosystem-ci run
📝 Ran ecosystem CI: Open
| suite | result | latest scheduled |
|---|---|---|
| language-tools | :x: failure | :x: failure |
| nuxt | :x: failure | :white_check_mark: success |
| pinia | :white_check_mark: success | :white_check_mark: success |
| quasar | :white_check_mark: success | :x: failure |
| router | :x: failure | :white_check_mark: success |
| test-utils | :x: failure | :x: failure |
| vant | :x: failure | :x: failure |
| vite-plugin-vue | :white_check_mark: success | :white_check_mark: success |
| vitepress | :x: failure | :white_check_mark: success |
| vue-i18n | :white_check_mark: success | :white_check_mark: success |
| vue-macros | :x: failure | :white_check_mark: success |
| vuetify | :x: failure | :x: failure |
| vueuse | :x: failure | :white_check_mark: success |
| vue-simple-compiler | :white_check_mark: success | :white_check_mark: success |
📝 Ran ecosystem CI: Open
| suite | result | latest scheduled |
|---|---|---|
| language-tools | :x: failure | :x: failure |
| nuxt | :white_check_mark: success | :white_check_mark: success |
| pinia | :white_check_mark: success | :white_check_mark: success |
| quasar | :white_check_mark: success | :white_check_mark: success |
| router | :white_check_mark: success | :white_check_mark: success |
| test-utils | :x: failure | :white_check_mark: success |
| vant | :white_check_mark: success | :white_check_mark: success |
| vite-plugin-vue | :white_check_mark: success | :white_check_mark: success |
| vitepress | :white_check_mark: success | :white_check_mark: success |
| vue-i18n | :white_check_mark: success | :white_check_mark: success |
| vue-macros | :x: failure | :white_check_mark: success |
| vuetify | :x: failure | :x: failure |
| vueuse | :x: failure | :white_check_mark: success |
| vue-simple-compiler | :white_check_mark: success | :white_check_mark: success |
📝 Ran ecosystem CI: Open
| suite | result | latest scheduled |
|---|---|---|
| language-tools | :x: failure | :x: failure |
| nuxt | :x: failure | :white_check_mark: success |
| pinia | :white_check_mark: success | :white_check_mark: success |
| quasar | :white_check_mark: success | :white_check_mark: success |
| router | :white_check_mark: success | :white_check_mark: success |
| test-utils | :x: failure | :white_check_mark: success |
| vant | :white_check_mark: success | :white_check_mark: success |
| vite-plugin-vue | :white_check_mark: success | :white_check_mark: success |
| vitepress | :white_check_mark: success | :white_check_mark: success |
| vue-i18n | :white_check_mark: success | :white_check_mark: success |
| vue-macros | :x: failure | :white_check_mark: success |
| vuetify | :x: failure | :x: failure |
| vueuse | :x: failure | :white_check_mark: success |
| vue-simple-compiler | :white_check_mark: success | :white_check_mark: success |
📝 Ran ecosystem CI: Open
| suite | result | latest scheduled |
|---|---|---|
| language-tools | :x: failure | :x: failure |
| nuxt | :x: failure | :white_check_mark: success |
| pinia | :x: failure | :white_check_mark: success |
| quasar | :x: failure | :white_check_mark: success |
| router | :x: failure | :white_check_mark: success |
| test-utils | :x: failure | :white_check_mark: success |
| vant | :x: failure | :white_check_mark: success |
| vite-plugin-vue | :x: failure | :white_check_mark: success |
| vitepress | :x: failure | :white_check_mark: success |
| vue-i18n | :stop_button: cancelled | :white_check_mark: success |
| vue-macros | :x: failure | :white_check_mark: success |
| vuetify | :x: failure | :white_check_mark: success |
| vueuse | :x: failure | :white_check_mark: success |
| vue-simple-compiler | :x: failure | :white_check_mark: success |
/ecosystem-ci run
📝 Ran ecosystem CI: Open
| suite | result | latest scheduled |
|---|---|---|
| language-tools | :x: failure | :x: failure |
| nuxt | :x: failure | :white_check_mark: success |
| pinia | :white_check_mark: success | :white_check_mark: success |
| quasar | :white_check_mark: success | :white_check_mark: success |
| router | :x: failure | :white_check_mark: success |
| test-utils | :x: failure | :white_check_mark: success |
| vant | :white_check_mark: success | :white_check_mark: success |
| vite-plugin-vue | :x: failure | :white_check_mark: success |
| vitepress | :white_check_mark: success | :white_check_mark: success |
| vue-i18n | :white_check_mark: success | :white_check_mark: success |
| vue-macros | :x: failure | :x: failure |
| vuetify | :x: failure | :x: failure |
| vueuse | :x: failure | :white_check_mark: success |
| vue-simple-compiler | :white_check_mark: success | :white_check_mark: success |
Will this help resolve question I asked? https://github.com/radix-vue/shadcn-vue/issues/277
Thanks for TS Magic 🪄
Will this help resolve question I asked? radix-vue/shadcn-vue#277
@jd-solanki no, the issue you linked is related with the vue compiler being able to infer props like that, bear in mind the compiler must know all the available props ahead of time ,to be able to generate the vue props object. When you extend or implement the type might lose that information.
@pikax Hey!
Please check the Svelte type helpers ComponentProps, and ComponentType in this source
https://github.com/wobsoriano/svelte-sonner/blob/main/src/lib/types.ts#L30C2-L31C51
What is the equivalent of this source with your Vue type helpers?
Thanks and sorry for tag :pray:
@sadeghbarati what type are you looking for, the two lines you sent one is the component and the other is props, we have:
- Component is ComponentType
- ComponentProps is ComponentProps
@pikax
I have something like this in Vue to get props, I have to use typeof keyword in ComponentProps right? but it doesn't work in types
export type ToastT<T extends Component = Component> = {
component?: T; // T is Vue component but how to get types without typeof
componentProps?: ComponentProps<typeof T>; // 'T' only refers to a type, but is being used as a value here.
}
Please take a look at svelte-sonner
- https://github.com/wobsoriano/svelte-sonner/blob/main/src/lib/state.ts
- https://github.com/wobsoriano/svelte-sonner/blob/main/src/lib/types.ts
Hey! Any news on this? It would be cool to use and test generics! Without this merge, https://github.com/vuejs/test-utils/issues/2254 cannot be closed, and we cannot really use generics. Can I help in some way to move this forward?
Hi, first of all thanks a lot for your work @pikax ❤️ Is there any plan on how to contine with this change? E.g. a roadmap or a release it's planned for. It looks like it's been stale for quiet a while and in my opinion it could bring a huge benefit to the ecosystem.