如何为Vue3泛型组件中的动态组件正确标注类型?
解决Vue3泛型日历组件的类型匹配问题
我开发了一个日历组件,接收外部传入的数据展示整月日期,有数据的日期会渲染指定组件。核心问题是日历组件无法预知每个日期插槽要渲染的组件类型,所以想用Vue3泛型组件实现:
- 数据以
Record<string, T[]>类型传入(键为Y-m-d格式日期字符串) - 要渲染的组件以能接收
T类型props的组件传入
组件代码
Calendar.vue
<script setup lang="ts" generic="T"> import {computed, DefineComponent, ref} from "vue"; import CalendarDay from "./CalendarDay.vue"; type Props<T> = { data: Record<string, T[]>, component: DefineComponent<T>, labelFn: (length: number, date: Date) => string, }; const props = withDefaults( defineProps<Props<T>>(), { labelFn: (count, date) => `${count} items on ${date.toLocaleDateString()}` } ); const currDate = new Date(); const currentValue = ref<{year: number, month: number}>( { year: currDate.getFullYear(), month: currDate.getMonth() + 1 } ); const dayArray = computed<number[]>(() => { const date = new Date(); const {year, month} = currentValue.value; const numbers: number[] = []; date.setFullYear(year, month-1, 1); while (date.getMonth() == month - 1) { numbers.push(date.getDate()); date.setDate(date.getDate() + 1); } return numbers; }) </script> <template> <div aria-live="polite" class="calendar-table"> <p v-for="i in 7" :key="i" class="calendar-table-header" aria-hidden="true"> <span class="long-name"> {{(new Date(2022, 7, i)).toLocaleString(undefined, {weekday: "long"})}} </span> <span class="short-name"> {{(new Date(2022, 7, i)).toLocaleString(undefined, {weekday: "short"})}} </span> </p> <CalendarDay v-for="day in dayArray" :label-fn="props.labelFn" :data="props.data" :component="props.component" :date="new Date(Date.UTC(currentValue.year, currentValue.month - 1, day))" /> </div> </template>
CalendarDay.vue
<script setup lang="ts" generic="T"> import { computed, DefineComponent} from "vue"; type Props<T> = { data: Record<string, T[]>, component: DefineComponent<T>, labelFn: (length: number, date: Date) => string, date: Date }; const props = defineProps<Props<T>>(); const dateStr = computed(() => `${props.date.toISOString().split('T')[0]}`); const value = computed((): T[] => props.data[dateStr.value] ?? []); const style = computed(() => props.date.getDate() == 1 ? {"grid-column-start": props.date.getDay()} : null) </script> <template> <div class="day" :class="{'has-items': value.length > 0}" :style="style" :aria-hidden="value.length == 0"> <div class="day-content" :aria-label="labelFn(value.length, props.date)"> <p class="date" aria-hidden="true"> {{ props.date.getDate() }} </p> <template v-for="val in value"> <component :is="component" v-bind="val"/> </template> </div> </div> </template>
TypeScript编译报错
Type 'DefineComponent<{}, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<ExtractPropTypes<{}>>, {}, {}>' is not assignable to type 'DefineComponent<{}>'.ts(2322) Calendar.vue(7, 5): The expected type comes from property 'component' which is declared here on type '{ data: Record<string, {}[]>; component: DefineComponent<{}>; labelFn: (length: number, date: Date) => string; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>' Argument of type 'T' is not assignable to parameter of type '(CreateComponentPublicInstance<Readonly<T extends ComponentPropsOptions<Data> ? ExtractPropTypes<T> : T>, {}, {}, ComputedOptions, ... 15 more ..., {} & EnsureNonVoid<...>> extends { ...; } ? Props : any) & Record<...>'. Type 'T' is not assignable to type 'CreateComponentPublicInstance<Readonly<T extends ComponentPropsOptions<Data> ? ExtractPropTypes<T> : T>, {}, {}, ComputedOptions, ... 15 more ..., {} & EnsureNonVoid<...>> extends { ...; } ? Props : any'.ts(2345) CalendarDay.vue(1, 34): This type parameter might need an `extends CreateComponentPublicInstance<Readonly<T extends ComponentPropsOptions<Data> ? ExtractPropTypes<T> : T>, {}, {}, ComputedOptions, ... 15 more ..., {} & EnsureNonVoid<...>> extends { ...; } ? Props : any` constraint. CalendarDay.vue(1, 34): This type parameter might need an `extends (CreateComponentPublicInstance<Readonly<T extends ComponentPropsOptions<Data> ? ExtractPropTypes<T> : T>, {}, {}, ComputedOptions, ... 15 more ..., {} & EnsureNonVoid<...>> extends { ...; } ? Props : any) & Record<...>` constraint.
正确的类型标注方案
问题根源在于对Vue的DefineComponent泛型参数理解有误:DefineComponent的泛型不是直接接收组件的props实例类型,而是接收组件的props定义(如ComponentPropsOptions)。要实现数据与组件props的类型匹配,需要调整泛型约束和类型关联方式:
- 重新定义泛型参数:让泛型
T代表组件的props类型,而非组件选项类型。 - 组件类型标注:使用
Component<T>(Vue提供的组件类型,能接收T作为props)替代DefineComponent<T>。 - 类型约束:确保传入的组件能接收
T类型的props。
修改后的代码如下:
调整后的Calendar.vue
<script setup lang="ts" generic="T"> import {computed, Component, ref} from "vue"; import CalendarDay from "./CalendarDay.vue"; type Props<T> = { data: Record<string, T[]>, // 使用Component<T>表示能接收T类型props的组件 component: Component<{ [K in keyof T]: T[K] }>, labelFn: (length: number, date: Date) => string, }; const props = withDefaults( defineProps<Props<T>>(), { labelFn: (count, date) => `${count} items on ${date.toLocaleDateString()}` } ); const currDate = new Date(); const currentValue = ref<{year: number, month: number}>( { year: currDate.getFullYear(), month: currDate.getMonth() + 1 } ); const dayArray = computed<number[]>(() => { const date = new Date(); const {year, month} = currentValue.value; const numbers: number[] = []; date.setFullYear(year, month-1, 1); while (date.getMonth() == month - 1) { numbers.push(date.getDate()); date.setDate(date.getDate() + 1); } return numbers; }) </script> <template> <div aria-live="polite" class="calendar-table"> <p v-for="i in 7" :key="i" class="calendar-table-header" aria-hidden="true"> <span class="long-name"> {{(new Date(2022, 7, i)).toLocaleString(undefined, {weekday: "long"})}} </span> <span class="short-name"> {{(new Date(2022, 7, i)).toLocaleString(undefined, {weekday: "short"})}} </span> </p> <CalendarDay v-for="day in dayArray" :label-fn="props.labelFn" :data="props.data" :component="props.component" :date="new Date(Date.UTC(currentValue.year, currentValue.month - 1, day))" /> </div> </template>
调整后的CalendarDay.vue
<script setup lang="ts" generic="T"> import { computed, Component} from "vue"; type Props<T> = { data: Record<string, T[]>, component: Component<{ [K in keyof T]: T[K] }>, labelFn: (length: number, date: Date) => string, date: Date }; const props = defineProps<Props<T>>(); const dateStr = computed(() => `${props.date.toISOString().split('T')[0]}`); const value = computed((): T[] => props.data[dateStr.value] ?? []); const style = computed(() => props.date.getDate() == 1 ? {"grid-column-start": props.date.getDay()} : null) </script> <template> <div class="day" :class="{'has-items': value.length > 0}" :style="style" :aria-hidden="value.length == 0"> <div class="day-content" :aria-label="labelFn(value.length, props.date)"> <p class="date" aria-hidden="true"> {{ props.date.getDate() }} </p> <template v-for="val in value" :key="val.id"> <component :is="component" v-bind="val"/> </template> </div> </div> </template>
关键说明
- 使用
Component<T>替代DefineComponent<T>:Component是Vue提供的更通用的组件类型,直接表示能接收T作为props的组件实例,避免了DefineComponent泛型参数的复杂约束。 - 泛型
T直接代表组件的props类型:这样传入的data数组元素类型T会和组件的props类型严格匹配,TypeScript会自动校验两者是否一致。 - 可选优化:给
v-for的循环项添加唯一key,符合Vue的最佳实践,避免渲染警告。
内容的提问来源于stack exchange,提问作者DrugCrazed
相关产品推荐
相关产品推荐

