Svelte
TypeScript в Svelte
Переиспользуемый компонент таблицы принимает массив строк и колбэк выбора. Без типов потребитель узнаёт, что передал не ту форму данных, только в рантайме по сломанной разметке. С типами TypeScript подсветит ошибку прямо в редакторе при передаче пропса. Атрибут generics делает таблицу типобезопасной для любого типа строки, не теряя проверок. А строгие правила (без any, без восклицательного знака, без as) заставляют выражать намерение через типы, а не глушить компилятор.
- Компонентные библиотеки: публичный API с точными типами пропсов ловит ошибки потребителя на этапе сборки
- Дженерик-таблицы и списки: один компонент типобезопасно работает с пользователями, заказами, товарами
- Формы: типы значений полей и ошибок не дают передать строку туда, где ждут число
- Рефакторинг: переименование поля в типе подсвечивает все компоненты, которые его используют
Предварительные знания
- Базовое знание TypeScript: интерфейсы, дженерики, объединения типов
- Понимание компонентов Svelte и того, как они принимают пропсы через `$props`
- Знание рун `$state` и `$derived` на уровне применения
Типизация `$props` и рун
TypeScript включается атрибутом lang в теге script. Пропсы компонента описываются типом, который передаётся как дженерик-параметр в `$props`. Это задаёт контракт: какие пропсы принимает компонент, какие из них обязательны, а какие имеют значения по умолчанию. Редактор и компилятор проверяют каждое использование компонента против этого типа.
Здесь count необязателен и имеет значение по умолчанию 0, а label и onSelect обязательны. Передача компонента без onSelect или с label не той формы вызовет ошибку типов на месте использования. Руны типизируются так же, как обычные значения: тип `$state` выводится из инициализатора, а при необходимости задаётся явным параметром типа.
Тип `$state<User | null>` задан явно, потому что инициализатор null сам по себе не подсказал бы тип User. Производное name читает user через optional chaining: user?.name, и подставляет запасное значение через оператор объединения с null. Здесь не понадобился ни восклицательный знак, ни каст: optional chaining и значение по умолчанию выражают обработку null безопасно и читаемо.
Параметр типа у `$state` нужен, когда инициализатор беднее целевого типа: например, null, пустой массив или пустой объект. Если инициализатор уже несёт полный тип, параметр избыточен, и тип выводится сам. Лишние аннотации только зашумляют код.
Состояние объявлено как let user = `$state<User | null>`(null), и нужно прочитать имя пользователя. Какой способ соответствует строгим правилам (без восклицательного знака и без as)?
Дженерик-компоненты через атрибут generics
Переиспользуемые компоненты вроде списка или таблицы должны работать с любым типом элемента, не теряя проверок. Для этого Svelte даёт атрибут generics у тега script. Он объявляет параметр типа, который можно использовать в типе пропсов. Компонент становится дженериком: тип элемента выводится из переданных данных, и связанные пропсы (например, колбэк выбора) проверяются против того же типа.
Параметр T объявлен в атрибуте generics и используется в типе пропсов: items это массив T, сниппет row принимает один аргумент типа T, а onSelect получает item того же T. Тип Snippet импортируется из svelte и параметризуется кортежем аргументов сниппета. При использовании компонента T выводится из items, и связанные пропсы проверяются согласованно.
Поскольку items типа User[], параметр T выводится как User. Поэтому в onSelect аргумент u имеет тип User, а в сниппете row параметр user тоже User, и обращение к user.name проверено типами. Если передать onSelect, ожидающий другой формы, компилятор сообщит об ошибке. Дженерик связывает все пропсы одним типом без единого каста.
Можно ограничить параметр типа через extends прямо в атрибуте: generics с записью вида T extends { id: number } требует, чтобы элемент имел поле id. Это полезно, когда компонент опирается на определённую структуру элемента, например использует item.id как ключ.
В List.svelte с атрибутом generics T тип пропсов задан как items: T[] и onSelect: (item: T) => void. Что произойдёт, если передать items типа User[], а onSelect, ожидающий аргумент типа Product?
Строгие правила: unknown, type guards, дженерики
Строгие правила запрещают три привычки: any, non-null assertion (восклицательный знак) и явный каст (as). Каждая из них отключает проверку, перекладывая риск в рантайм. Вместо них есть безопасные инструменты. Неизвестные данные описывают типом unknown, а не any: unknown заставляет сузить тип прежде, чем им пользоваться. Сужение делают через type guard, то есть проверку, которая сообщает компилятору фактический тип.
Функция isUser это type guard: её возвращаемый тип value is User говорит компилятору, что при истинном результате value имеет тип User. После проверки isUser(raw) в ветке true переменная raw уже сужена до User без каста. Ответ на вход неизвестной формы это null, а не падение. Так данные из внешнего источника проходят валидацию, а не проскальзывают через as.
| Запрещено | Почему опасно | Чем заменить |
|---|---|---|
| any | Отключает проверку типов целиком | unknown с сужением через type guard |
| Восклицательный знак (assertion) | Обещает ненулевое значение без гарантии | Optional chaining и проверка на null |
| as Type (каст) | Заставляет компилятор поверить без проверки | Type guard или дженерик-параметр |
Дженерики заменяют касты там, где тип должен прийти от вызывающей стороны. Вместо того чтобы внутри функции приводить значение к нужному типу через as, функцию делают дженериком и принимают тип параметром. Тогда тип задаёт вызывающий код, а тело функции остаётся типобезопасным. Это переносит ответственность за тип туда, где он действительно известен.
Если компилятор ругается, правильная реакция это уточнить типы, а не заглушить ошибку через any, восклицательный знак или as. Заглушка убирает сообщение, но оставляет реальный риск в рантайме. Type guard, optional chaining и дженерик устраняют и сообщение, и риск, выражая намерение в самих типах.
Из внешнего API приходит значение неизвестной формы, и его нужно использовать как User. Какой подход соответствует строгим правилам?
Связь с другими темами
Типизация надстраивается над компонентами и делает паттерны безопасными:
- Компоненты — Типы пропсов описывают контракт компонента, заданный через `$props`
- Паттерны компонентов — Дженерики и типы сниппетов делают render-props и headless-компоненты типобезопасными
- Введение в руны — Руны типизируются как обычные значения, и строгие типы раскрывают их API
Итог
- lang ts в теге script включает TypeScript, и пропсы типизируются через дженерик-параметр у `$props`
- Дженерик-компоненты объявляются атрибутом generics у тега script, что делает компонент типобезопасным для любого типа данных
- Руны типизируются как обычные значения: тип выводится из инициализатора или задаётся явным параметром типа
- Без any: неизвестные данные описывают через unknown и сужают type guard-ами, сохраняя проверки типов
- Без non-null assertion и без as: вместо них используют optional chaining, type guards и дженерики, выражая намерение типами
Связанные уроки
- sv-03-components — Типизация пропсов опирается на понимание самих компонентов и того, как они принимают входные данные
- sv-35-component-patterns — Дженерик-компоненты и типы сниппетов делают паттерны из предыдущего урока безопасными в использовании
- sv-05-runes-intro — Руны типизируются как обычные значения, и строгая типизация раскрывает их полностью