- Start Date: 2022-03-17
- Target Major Version: 3.x
- Reference Issues: vuejs/core#3102
- Implementation PR: (leave this empty)
Allow to infer generics when passing props on the <template>
, JSX
or h
.
This will only affect typings used by typescript (IDE, TSX, etc) and don't bring any runtime cost.
There's 2 use cases:
- script setup - This case requires heavy lifting on IDEs to enhance types.
- defineComponent - This will require the users to enhance types, minimal work on the IDE might be required, eg: Volar should work out-of-box.
This RFC will target both User and IDE, distinction will be explicit
Simple T
<script setup lang="ts" generic="T">
defineProps<{
list: T[],
modelValue?: T
}>
defineEmits<{
(e: `update:ModelValue`, a: T): void
}>()
</script>
With extends
<script setup lang="ts" generic="T extends string">
defineProps<{
types: T
}>
</script>
Multiple (from vuejs/core#3102)
<script
setup
lang="ts"
generic="Clearable extends boolean, ValueType extends string | number | null | undefined"
>
type OnChange<ValueType, Clearable> = Clearable extends true
? (value: ValueType | null) => void
: (value: ValueType) => void;
defineProps<{
clearable?: Clearable;
value?: ValueType;
onChange?: OnChange<ValueType, Clearable>;
}>
</script>
with imported types + extends
The type can be imported either inside of the
script setup
or in otherscript
<script setup lang="ts" generic="T extends MyInterface">
import { MyInterface } from './types'
defineProps<{
types: T
}>
</script>
This is not user friendly and is more intended to be used for library creators
Limitations: We must have a generic constructor to be able to infer types when using TSX, because of the
defineComponent
you cannot have a constructor (unless with Class API or by short the type inference), meaning the only solution we have is by appending the correct type to thedefineComponent
and let typescript use that.
Simple T
declare class CompProps<T> extends ComponentPropsOverride<{ list: T[], modelValue?: T}> {};
export default makeGeneric(
defineComponent({
props: {
list: {
type: Array,
required: true
},
modelValue: null
}
})
)).withGenerics<typeof CompProps>()
Multiple (from vuejs/core#3102)
type OnChange<ValueType, Clearable> = Clearable extends true
? (value: ValueType | null) => void
: (value: ValueType) => void;
declare class CompProps<
Clearable extends boolean,
ValueType extends string | number | null | undefined> extends ComponentPropsOverride<{
clearable?: Clearable;
value?: ValueType;
onChange?: OnChange<ValueType, Clearable>;
}> {};
export default makeGeneric(
defineComponent({
props: {
clearable: Boolean,
value: [String, Number],
onChange: Function
}
})
)).withGenerics<typeof CompProps>()
Provide a way to support Generics
, since Vue3 has first class support for Typescript the only place it lacks is allowing developers to clearly express the types the component expects, instead of relying on catch all types (eg: Object, Array, null, etc).
The usage with defineComponent
is not great but it works today (using Volar), because of the requirement of having a constructor that has a generic, there's no other way (that I can think of) to solve this in a cleaner way.
I expect the usage with script setup
will bring this feature to be used more, since the API is simple and we have already the step of compiling it.
To be able to understand this RFC we first need to understand that Types (typescript) and Implementation(javascript), might need to be handle differently, sometimes we need to patch the defineComponent
for it to reflect a more accurate component.
IDE extensions (eg: Volar), will require to do heavy lifting here, the design will be provided in TSX since Vue3 already supports first class and it should be supported today - if you doubt go to typescript playground and paste the results there.
This will add a new attribute named generic
, inside of that argument is a valid typescript language and ideally we should have intellisense.
generic
value is basically the content inside class MyClass<${genericValue}>
.
Arguments specified inside of generic
will be available and have intellisense inside of the script setup
.
This conversions are high-level and simplified, but still be valid on typescript only environment
Typescript to component - NOT GENERIC COMPONENT
<script setup lang="ts" generic="T extends string">
defineProps<{
modelValue: T
}>
// code
</script>
Converted to:
export default (<T extends string>() => {
return defineComponent({
props: {
modelValue: String as Prop<T>
}
})
})()
Converting script setup
to typescript compatible code won't make your component be able to infer the types when used on the <template>
, you will only get statically typed components (Generically Typed Vue.js Components)
SFC
<script setup lang="ts" generic="T extends string | number">
defineProps<{
list: T[],
modelValue?: T
}>
</script>
import { defineComponent, PropType } from 'vue'
const Comp = (<T extends string | number>() => {
return defineComponent({
props: {
list: {
type: Array as PropType<T[]>,
required: true
},
modelValue: null as unknown as PropType<T>
}
})
})()
declare class CompGeneric<T extends string | number> {
readonly $props: {
list: T[]
modelValue?: T
}
}
export default Comp as typeof Comp & typeof CompGeneric // override to generic `$props`
// test
declare const MyComp: typeof Comp & typeof CompGeneric
;<MyComp list={[1]} />
;<MyComp list={[1]} modelValue={1} />
// @ts-expect-error
;<MyComp list={[1]} modelValue={'1'} />
SFC
<script setup lang="ts" generic="T extends MyItem">
interface MyItem {
name: string;
foo: number
}
defineProps<{
list: T[],
modelValue?: T
}>
</script>
import { defineComponent, PropType } from 'vue'
interface MyItem {
name: string
foo: number
}
const Comp = (<T extends MyItem>() => {
return defineComponent({
props: {
list: {
type: Array as PropType<T[]>,
required: true
},
modelValue: null as unknown as PropType<T>
}
})
})()
declare class CompGeneric<T extends MyItem> {
readonly $props: {
list: T[]
modelValue?: T
}
}
export default Comp as typeof Comp & typeof CompGeneric // override to generic `$props`
// test
declare const MyComp: typeof Comp & typeof CompGeneric
;<MyComp list={[{ name: 'test', foo: 1 }]} />
;<MyComp
list={[{ name: 'test', foo: 1 }]}
modelValue={{ name: 'test', foo: 1 }}
/>
// @ts-expect-error
;<MyComp list={[1]} modelValue={'1'} />
// @ts-expect-error
;<MyComp list={[{ name: 'test', foo: 1 }]} modelValue={1} />
Is not required any further work on IDE, the only work needed is to expose this types on Vue3 package:
// not required, just a sugar for users
export declare abstract class ComponentPropsOverride<Props = {}> {
readonly $props: Props
}
export declare function makeGeneric<T>(c: T): {
withGenerics<Props extends { prototype: ComponentPropsOverride<any> }>(): T &
Props
}
import {
defineComponent,
DefineComponent,
ComponentPropsOverride,
makeGeneric
} from 'vue'
// user
type OnChange<ValueType, Clearable> = Clearable extends true
? (value: ValueType | null) => void
: (value: ValueType) => void
interface GenericProp<Clearable, ValueType> {
clearable?: Clearable
value?: ValueType
onChange?: OnChange<ValueType, Clearable>
}
declare class CompProps<
Clearable extends boolean,
ValueType extends string | number | null | undefined
> extends ComponentPropsOverride<GenericProp<Clearable, ValueType>> {}
const MyGenericComp = makeGeneric(
defineComponent({
props: {
test: Boolean
}
})
).withGenerics<typeof CompProps>()
;<MyGenericComp
value={'sss'}
clearable
onChange={(a) => {
expectType<'sss' | null>(a)
}}
/>
This is the way with less drawbacks, DX should be great and it will be the preferred way.
This brings quite a lot of work and DX is not amazing, this will require the user to be quite strict to prevent wrong type inferences, because props will be duplicated, errors are easy to show up.
defineComponent
changes can be already be done on the userLand
, but it would be good to provide guidance on how to do this, I expect libraries to take advantage of this to provide better type for their components
vuejs#310 - Class alternative
This functionality is additive, we should only update the components that are actually generic.
Naming suggestions or improvements on the API are welcome.