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 — Руны типизируются как обычные значения, и строгая типизация раскрывает их полностью
TypeScript в Svelte

0

1

Войти