You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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的类型匹配,需要调整泛型约束和类型关联方式:

  1. 重新定义泛型参数:让泛型T代表组件的props类型,而非组件选项类型。
  2. 组件类型标注:使用Component<T>(Vue提供的组件类型,能接收T作为props)替代DefineComponent<T>。
  3. 类型约束:确保传入的组件能接收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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 05:10:54