Angular

TypeScript в Angular: строгая типизация

В коде встречается строка с restApi.data as User. Спустя месяц форма API изменилась, поле переименовали, но приведение as заглушило компилятор - и ошибка всплыла в проде как undefined у пользователя. Каждый as, каждый восклицательный знак и каждый any - это место, где разработчик сказал компилятору поверь мне, и тем самым отключил единственную защиту, ради которой выбран TypeScript. Строгий режим Angular и осознанная типизация позволяют ни разу не прибегнуть к этим лазейкам: вместо обещаний компилятору даются доказательства через type guards и точные типы.

  • Крупные кодовые базы (Angular-монорепо, корпоративные приложения) включают strict и strictTemplates, чтобы ловить ошибки до ревью
  • Команды с правилом запрета any/as/non-null в ESLint: лазейки типов запрещены политикой проекта, как в этом курсе
  • Типобезопасные реактивные формы: FormGroup с типизированной моделью ловит опечатку в имени контрола на этапе компиляции
  • Дискриминированные объединения для состояний загрузки (loading/loaded/error): компилятор заставляет обработать каждый случай
  • strictTemplates ловит обращение к несуществующему полю прямо в HTML-шаблоне, а не в рантайме у пользователя

Предварительные знания

  • TypeScript на уровне интерфейсов, union-типов и дженериков
  • Signal inputs/outputs и реактивные формы Angular на уровне идеи
  • Понимание разницы между ошибкой компиляции и ошибкой рантайма
  • Компоненты

Как строгость стала нормой в Angular

Долгое время Angular-проекты создавались с мягкими настройками типов, и any расползался незаметно. В Angular 10 (2020) появился флаг strict при создании проекта, а с Angular 12 строгий режим стал значением по умолчанию для новых проектов. Параллельно созрел strictTemplates - проверка типов внутри HTML-шаблонов, превращающая шаблон в типизированный код. К 2025-2026 году строгий режим вместе с strictTemplates стал отраслевой нормой, а такие лазейки, как any, as и non-null assertion, во многих командах прямо запрещены линтером.

Строгий режим и strictTemplates

Флаг strict в tsconfig - это не одна проверка, а набор: strictNullChecks (null и undefined не входят молча в другие типы), strictFunctionTypes, noImplicitAny и другие. Главная из них - strictNullChecks: она заставляет явно обрабатывать случай отсутствия значения, и именно она убирает целый класс ошибок вида cannot read property of undefined. Angular добавляет к этому strictTemplates - проверку типов внутри HTML-шаблонов компонента.

strictTemplates превращает шаблон в типизированный код. Обращение к полю, которого нет в компоненте, неверный тип входа дочернего компонента, ошибка в имени свойства - всё это становится ошибкой компиляции, а не сюрпризом в рантайме у пользователя. Шаблон перестаёт быть строкой и проверяется так же строго, как и TypeScript-файл.

Без strictNullChecks тип string молча включает null. С ним тип, который может быть пустым, обязан быть string | null, и компилятор требует обработать null до использования. Это переносит проверку с этапа жалоб пользователя на этап компиляции.

Что именно добавляет angularCompilerOptions.strictTemplates поверх обычного строгого режима TypeScript?

Вместо any: unknown и сужение типов

any - это не тип, а отказ от типизации: с ним компилятор перестаёт проверять что угодно, и любая опечатка проходит молча. Когда тип значения действительно неизвестен заранее (ответ внешнего API, данные из JSON), правильный инструмент - unknown. В отличие от any, с unknown ничего нельзя сделать, пока тип не сужен явной проверкой. Это заставляет описать форму данных один раз и проверить её, а дальше работать с конкретным типом.

Функция isUser - это user-defined type guard: её возвращаемый тип value is User говорит компилятору, что внутри истинной ветки value безопасно считать User. После проверки доступ к payload.name типобезопасен без единого приведения. Если бы payload был any, ошибка в имени поля прошла бы молча; с unknown компилятор требует доказать форму прежде, чем работать с данными.

  • any — Отключает проверку типов полностью. Опечатки и неверный доступ проходят молча. Ошибка всплывает в рантайме у пользователя.
  • unknown + type guard — Требует сузить тип до использования. Форма данных описана и проверена один раз. Дальше работа с конкретным типом, ошибки ловятся компилятором.

Соблазн написать payload as User вместо проверки велик, но as лишь обещает тип, не проверяя его в рантайме. Если форма данных не совпала, приложение продолжит работать с неверным предположением до первой попытки прочитать отсутствующее поле. Type guard проверяет реально, поэтому безопасен.

Ответ внешнего API приходит без гарантий формы. Почему unknown с type guard безопаснее, чем any или приведение as?

Без non-null assertion и без приведений as

Восклицательный знак после выражения (non-null assertion) говорит компилятору это точно не null, поверь мне. Но если значение всё-таки окажется null, проверка отключена, и ошибка прорвётся в рантайм. Вместо обещания нужна проверка: явное сравнение с null, optional chaining или ранний выход. После проверки компилятор сам сужает тип и снимает null, никакого ! не требуется.

В greet ранний выход при null оставляет ниже по коду тип User без null, и user.name безопасен без !. В firstChildText цепочка optional chaining с ?? '' проходит весь путь, который может прерваться на null, и возвращает строку. Ни одной лазейки: компилятор видит каждую возможную пустоту и убеждается, что она обработана.

Закономерность одна: и !, и as отключают защиту в обмен на удобство. Замена всегда из двух шагов - проверить (сравнение, type guard, поле-дискриминатор), а затем дать компилятору сузить тип самому. Получается короче в рассуждении и безопаснее в рантайме.

У значения тип User | null. Как обратиться к user.name без non-null assertion (!) и без приведения as?

Дискриминированные объединения и типизация форм

Состояние загрузки данных часто моделируют тремя булевыми полями: loading, loaded, error. Это допускает невозможные комбинации (loading и error одновременно) и заставляет писать защитные проверки. Дискриминированное объединение описывает ровно допустимые состояния через общее поле-дискриминатор. Компилятор тогда сужает тип по этому полю и требует обработать каждый вариант.

Поле data существует только в ветке loaded, message - только в error, поэтому обратиться к ним в неправильном состоянии физически нельзя: компилятор не даст. Если позже добавить новый статус, switch без его обработки вызовет ошибку типов при включённой строгости. Невозможные состояния стали непредставимыми.

Типизированная FormGroup ловит опечатку в имени контрола на компиляции: form.controls.naem не существует в типе ProfileForm. Флаг nonNullable у контрола убирает null из типа value, поэтому form.controls.name.value сразу string, без проверки на null и без !. Типы форм и дискриминированные объединения вместе закрывают большую часть рантайм-ошибок ещё до запуска.

Чем дискриминированное объединение для состояния загрузки лучше трёх отдельных булевых флагов loading/loaded/error?

Связь с другими темами

Урок задаёт типобезопасный фундамент для остального кода. Связи:

  • SignalStore — Стор выводит типы состояния из начальной формы, строгий режим делает их использование безопасным
  • Доступность — Следующий слой качества после строгой типизации - доступность интерфейса
  • Тестирование — Точные типы сокращают класс ошибок, которые иначе пришлось бы ловить тестами

Итог

  • Строгий режим (strict) включает набор проверок (strictNullChecks и другие), а strictTemplates распространяет типизацию на HTML
  • any отключает проверку типов целиком; вместо него используют unknown с последующим сужением через type guards
  • Non-null assertion (!) лишь заглушает компилятор; правильный путь - проверка на null, optional chaining или ранний выход
  • Приведение as обещает тип без доказательства; type guard доказывает его и сужает безопасно
  • Дискриминированные объединения моделируют взаимоисключающие состояния так, что компилятор требует обработать каждое
  • Generics типизируют переиспользуемый код (компоненты, сервисы) без потери конкретики и без any

Связанные уроки

  • ng-03-components — Типизация касается inputs/outputs компонентов и их шаблонов
  • ng-39-ngrx-signalstore — SignalStore выводит типы состояния, и строгая типизация делает доступ к ним безопасным
  • ng-42-accessibility-aria — После строгой типизации идём к доступности - следующему слою качества
TypeScript в Angular: строгая типизация

0

1

Войти