Vue
TypeScript в Vue
Компонент таблицы принимает массив строк и колонок, рисует ячейки и эмитит выбранную строку. Без типов любая опечатка в имени пропса всплывает только в рантайме, эмит с неправильной нагрузкой проходит молча, а внутри обработчика приходится верить, что значение точно не null. Соблазн заглушить ошибку через as или ! велик, но каждый такой обход выключает проверки ровно там, где они нужнее всего. Этот урок про то, как типизировать пропсы, эмиты и ref так, чтобы компилятор ловил ошибки заранее, и при этом не написать ни одного any, ни одного non-null assertion и ни одного каста.
- Дизайн-системы на Vue: типизированные пропсы документируют контракт компонента прямо в коде
- defineProps<T>() и defineEmits<T>(): компилятор SFC выводит типы пропсов и событий из дженерик-аргумента
- Дженерик-компоненты: типобезопасная таблица и select, работающие с любым типом строки
- Discriminated unions: состояние загрузки idle | loading | success | error без any и без кастов
- Строгий tsconfig в продакшен-кодовых базах: запрет any, noUncheckedIndexedAccess и strictNullChecks
Предварительные знания
- Уверенная работа со script setup и компонентами SFC
- Базовый TypeScript: интерфейсы, дженерики, union-типы
- Понимание пропсов и событий компонента на уровне идеи
Типизированные defineProps и defineEmits
В script setup пропсы объявляются макросом defineProps. У него две формы: рантайм-объект с описанием полей и типовая форма через дженерик-аргумент. Типовая форма предпочтительна: контракт описывается интерфейсом или type, а компилятор SFC сам выводит из него типы пропсов. Значения по умолчанию задаются через withDefaults, не ломая типизацию.
События объявляются макросом defineEmits в типовой форме. Дженерик-аргумент описывает сигнатуру каждого события: имя и тип нагрузки. После этого вызов emit с неизвестным именем или неправильным типом аргумента не скомпилируется. Это превращает события в такой же проверяемый контракт, как и пропсы.
Типовая форма макросов работает на уровне компилятора SFC: типы существуют только во время сборки и не превращаются в рантайм-проверки пропсов. Поэтому валидацию данных, пришедших из внешнего источника (ответ API, query-параметры), всё равно делают отдельно, например через схему. Типы защищают код во время разработки, а не данные во время выполнения.
В чём преимущество типовой формы defineProps<Props>() перед рантайм-объектом?
Типизация ref и шаблонных ссылок
ref выводит тип из начального значения: ref(0) это Ref<number>, ref('') это Ref<string>. Когда тип шире начального значения (например, поле может стать строкой, а стартует как null), его задают явным дженериком. Это снимает соблазн позже воткнуть каст: тип сразу описан правильно.
Особый случай это ссылка на DOM-элемент или дочерний компонент через ref в шаблоне. До монтирования такая ссылка равна null, поэтому её тип всегда включает null. Здесь возникает главный соблазн поставить non-null assertion, но правильный путь это явная проверка. Optional chaining или ранний возврат сужают тип безопасно.
useTemplateRef из Vue 3.5 типизирует шаблонную ссылку дженериком и возвращает Ref<T | null>. Тип честно включает null, потому что до монтирования элемента нет. Проверка if (ref.value) или optional chaining ref.value?.focus() решает это без non-null assertion.
Шаблонная ссылка на input имеет тип HTMLInputElement | null. Как вызвать focus, не нарушая запрет на non-null assertion?
Дженерик-компоненты и discriminated unions
Переиспользуемый компонент вроде таблицы или select должен работать с любым типом строки и при этом сохранять типобезопасность: эмит выбранной строки обязан иметь конкретный тип, а не any. Это решается дженерик-компонентом. Атрибут generic в теге script переносит параметр типа внутрь, и пропсы, эмиты и слоты становятся параметризованными этим типом.
Теперь при использовании компонента с массивом User событие select типизировано как User, а слот отдаёт row типа User. Параметр T выводится из переданного массива автоматически. Никаких any и кастов: компилятор знает конкретный тип строки в каждом месте использования.
Второй частый источник кастов это состояние с несколькими формами: загрузка может быть в процессе, успешной с данными или ошибочной с сообщением. Соблазн объявить data как T | null и error как string | null и потом всюду проверять оба поля. Чище смоделировать это discriminated union по полю status: тогда в ветке success компилятор сам знает, что data есть, а в ветке error знает про message.
Каст as и non-null assertion ! не исправляют тип, они затыкают компилятор. Если приходится их писать, это сигнал, что тип смоделирован неточно. Discriminated union, type guard или дженерик почти всегда дают тот же результат без обмана проверок. any же отключает типизацию целиком и заражает всё, к чему прикасается.
Состояние запроса бывает loading, success с данными и error с сообщением. Как смоделировать его без any и кастов?
Связь с другими темами
Урок опирается на script setup и питает паттерны и тесты:
- script setup — Макросы defineProps и defineEmits живут именно в script setup, типизация строится на них
- Паттерны компонентов — Дженерик-компоненты и типизированные слоты это фундамент переиспользуемых API
- Тестирование — Типизированный контракт пропсов и эмитов упрощает написание точных тестов
Итог
- defineProps<T>() принимает тип через дженерик-аргумент, и компилятор SFC выводит типы пропсов без отдельного объекта options
- defineEmits<T>() задаёт сигнатуры событий, после чего вызов emit проверяется на имя и тип нагрузки
- ref выводит тип из начального значения, а при необходимости тип задаётся явным дженериком ref<T>()
- Дженерик-компонент через <script setup lang="ts" generic="T"> переносит параметр типа в пропсы и слоты, что даёт типобезопасную таблицу или select для любого типа строки
- Запрещены any, non-null assertion ! и каст as: вместо них применяются type guards, дискриминированные union-типы и дженерики
- Discriminated union по полю status позволяет компилятору сузить тип в каждой ветке без единого каста
Связанные уроки
- vue-41-testing — Строго типизированные пропсы и эмиты дают тестам предсказуемые контракты компонентов
- vue-42-component-patterns — Дженерик-компоненты и типизированные слоты это основа переиспользуемых паттернов