У меня есть компонент «календарь». Этот компонент будет получать некоторые данные от вызывающего абонента и отображать все дни в месяце — если для этого дня существуют какие-то данные, то я хочу затем отобразить другой компонент. Проблема в том, что мой компонент календаря не знает, какой тип компонента он должен отображать в каждом слоте дня.
Я ожидал, что смогу просто использовать универсальный компонент 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)
Как правильно это ввести, чтобы компилятор мог убедиться, что данные соответствуют реквизитам для этого компонента?
Тип 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']>>};
поэтому неправильные типы будут содержать ошибки в данных.