TypeScript во Vue
Вопрос
Как типизировать <script setup>, ссылку на дочерний компонент и composable, который принимает и значение, и ref?
Короткий ответ
Пропсы описывают типом в defineProps<Props>(). Значения по умолчанию для объектов и массивов — через withDefaults, чтобы дефолт был фабрикой, а не общей ссылкой. События — через defineEmits. С Vue 3.3 короткая форма кортежей: defineEmits<{ change: [id: number] }>().
Ссылка на DOM: ref<HTMLInputElement | null>(null). Ссылка на компонент: ref<InstanceType<typeof MyModal> | null>(null). Методы, открытые через defineExpose, тогда видны у modalRef.value. Тип пропсов чужого компонента без ручного экспорта берут как ComponentProps<typeof MyButton>.
Универсальный список или селект помечают generic на <script setup>. Родитель передаёт User[], и событие выбора возвращает User, а не any.
Composable, которому всё равно, пришли примитив, ref или геттер, принимает MaybeRefOrGetter<T> и читает значение через toValue.
Разбор
Дженерик-компонент:
<script setup lang="ts" generic="T extends { id: number | string; label: string }">
defineProps<{
items: T[]
selectedItem: T | null
}>()
const emit = defineEmits<{
select: [item: T]
}>()
</script>MaybeRefOrGetter<string> — это string | Ref<string> | (() => string). Внутри composable не пишут цепочку isRef.
Поля route.meta по умолчанию почти пустые. Свои флаги добавляют расширением модуля, не заменой:
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
role?: 'admin' | 'user' | 'guest'
}
}Импорт 'vue-router' нужен, чтобы TypeScript дополнил модуль. Так же расширяют vue: ComponentCustomProperties для $api на экземпляре.
customRef сам решает, когда звать track и trigger. Так делают debounced-поле: сеттер пишет значение спустя задержку и только тогда будит подписчиков. Дженерик на фабрике (useDebouncedRef<T>) сохраняет тип значения.
Готовые утилиты Omit, Pick, Partial, Record, ReturnType для форм и фильтров разобраны в разделе условных типов. Во Vue тот же приём: Omit<User, 'id' | 'createdAt'> для формы создания, Partial<Filters> для необязательных полей фильтра.
На что смотрят на senior
InstanceType видит только то, что компонент отдал наружу. Закрытый ref из <script setup> с родителя не типизируется и не должен читаться. Публичный метод — осознанный defineExpose.
Module augmentation живёт в *.d.ts, который входит в компиляцию. Файл рядом с фичей, не подключённый в tsconfig, молча ничего не расширит.
Дженерик на каждый презентационный компонент шумит. Он нужен, когда один компонент обслуживает разные сущности и тип события должен совпасть с типом элемента. Экран заказа знает свой Order и без параметра типа.
Типичные ошибки
- Писать
inject('key')строкой и приводить результат черезas. - Забывать
| nullу template ref: до монтирования значения нет. - Аннотировать аргумент composable как
Ref<T>и ломать вызов с обычной строкой. - Расширять
RouteMetaчерез новыйinterfaceв своём файле безdeclare module.
Задача
const modal = ref(null) указывает на компонент с defineExpose({ open }). Почему modal.value?.open() для TypeScript — ошибка и чем типизировать ref?
Показать ответ
ref(null) выводится как Ref<null>. У null нет open.
Нужно ref<InstanceType<typeof MyModal> | null>(null). InstanceType видит только то, что отдано через defineExpose. Закрытый ref из <script setup> снаружи так не достать.