Как ввести динамический компонент в общий компонент Vue?

У меня есть компонент «календарь». Этот компонент будет получать некоторые данные от вызывающего абонента и отображать все дни в месяце — если для этого дня существуют какие-то данные, то я хочу затем отобразить другой компонент. Проблема в том, что мой компонент календаря не знает, какой тип компонента он должен отображать в каждом слоте дня.

Я ожидал, что смогу просто использовать универсальный компонент Vue3 и сказать:

  • Данные можно передавать как реквизит типа Record<string, T[]> (где ключ — это просто Y-m-d).
  • Компонент, который я хочу использовать, будет передан как реквизит типа DefineComponent<T>
// 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>

Хотя это работает в браузере, компилятор машинописного текста жалуется:

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 потребителя компонента находится здесь (на этой игровой площадке Vue)

Как правильно это ввести, чтобы компилятор мог убедиться, что данные соответствуют реквизитам для этого компонента?


1
51
1

Ответ:

Решено

Детская площадка

Тип Component работает нормально:

Календарь.vue

<script setup lang = "ts" generic = "T extends Record<string, any>">
import {type Component, computed, ref} from "vue";
import CalendarDay from "./CalendarDay.vue";

type Props<T> = {
    data: Record<string, T[]>,
    component: Component<T>,
    labelFn: (length: number, date: Date) => string,
};

...

Таким образом, вы можете использовать его без каких-либо типов:

<script setup lang = "ts">
import Calendar from './Calendar.vue';
import SessionLink from "./SessionLink.vue";

//type Props = {days: Record<string, Array<InstanceType<typeof SessionLink>['$props']>>};

const props /*: Props */ = {
  days: {
    "2024-08-31": [
        {
          some: 'some',
          data: 'data'
        }
    ]
  }
}

function label(count: number, date: Date) {
    return `${count} sessions on ${date.toLocaleDateString()}`;
}
</script>

<template>
  <Calendar :data = "props.days" :component = "SessionLink" :label-fn = "label" />
</template>

Если вы укажете неправильные типы, :component будет ошибочным, если вы хотите более точный контроль, используйте

type Props = {days: Record<string, Array<InstanceType<typeof SessionLink>['$props']>>};

поэтому неправильные типы будут содержать ошибки в данных.