Как ввести динамический компонент в общий компонент 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)

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

Можете ли вы подтвердить, что проблема существует в сборке? Потому что это может быть связано с редактором, в котором вы видите ошибку.

Estus Flask 28.08.2024 19:24

Да, я получаю то же самое на местном уровне. Сообщения немного отличаются, но это те же строки, что и ошибка.

DrugCrazed 28.08.2024 23:06

Как именно? С vue-tsc? Пожалуйста, добавьте в вопрос соответствующие части кода, он должен быть понятным, если внешняя ссылка недоступна.

Estus Flask 28.08.2024 23:37

Я полагаю, вы можете просто использовать тип компонента вместо DefineComponent<T>. Даже если бы то, что вы пробовали, поддерживалось, я сомневаюсь, что вы могли бы получить пользу от подробного типа, который предоставляет универсальный тип. Поддержка TS в шаблонах пока новая, но вы можете открыть проблему

Estus Flask 28.08.2024 23:47
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
4
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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']>>};

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

Другие вопросы по теме