Система типов
TypeScript — это не язык со статической типизацией поверх рантайма JavaScript, а отдельный слой проверок, который целиком удаляется перед выполнением. Компилятор состоит из двух почти независимых частей: checker читает ваши аннотации, строит из них граф типов и выносит вердикт, а emitter просто снимает с кода всё типовое и печатает обычный JavaScript. Ни одна проверка не переживает эту границу. Отсюда и главный водораздел темы: любое утверждение о типе, которое вы делаете кодом (as, !, interface, generic-параметр), — это утверждение для checker'а, и рантайм о нём не узнает. Всё, что нужно знать во время выполнения, придётся выразить значениями: class, дискриминантным полем, схемой-валидатором.
Вторая половина темы — про то, что и внутри самих проверок гарантий меньше, чем кажется. Система типов TypeScript структурна (тип — это форма, а не имя, поэтому type UserId = string и type OrderId = string — один и тот же тип) и намеренно несостоятельна: массивы ковариантны, параметры метода-сокращения остаются бивариантными даже под strictFunctionTypes, arr[10] по умолчанию типизируется как T, а не T | undefined, а any — вообще не тип, а дыра, присваиваемая в обе стороны и заражающая всё, к чему прикоснулась. Это не баги: команда TypeScript явно записала «доказуемо корректная система типов» в не-цели языка, выбрав баланс между строгостью и продуктивностью на существующем JavaScript. Работать с этим можно — но только зная, где ровно проходят дыры и чем каждая из них закрывается: unknown на границе, satisfies вместо аннотации, бренд вместо голого string, валидатор вместо as. Разбор по слоям — ниже.
Карта темы
- Стирание типов — что
tscфизически удаляет из вывода, что переживает компиляцию и почему нельзя ни ветвиться по типу, ни сделатьnew T(). - Утверждение типа (as) —
asкомпилируется в ничто и не может провалиться, чем он отличается от приведения и почемуas unknown as T— это выключатель проверок. - as const — const-assertion выключает расширение типа, даёт
readonlyи кортежи и позволяет вывести union прямо из значения. - satisfies — оператор из TypeScript 4.9, который проверяет выражение по типу, не меняя выведенный тип выражения.
- Структурная типизация — тип опознаётся по форме, а не по имени; почему в TypeScript нет номинальности и где она всё же прорывается.
- Брендированные типы — фантомное поле в пересечении, умный конструктор как единственное законное место для
as, и цена вопроса. - Распространение any — почему один
anyна границе отключает проверки в коде, который его не упоминает, и почемуunknown— правильная замена. - Намеренная несостоятельность — бивариантность методов, ковариантность массивов, непроверяемый индекс и остальные дыры как осознанный компромисс.
Частые ошибки и ловушки
| Ошибка | Последствие | |
|---|---|---|
Считать, что as User проверяет данные во время выполнения | as стирается и не может провалиться — падение случится позже и в другом файле | |
Писать await res.json() as User вместо разбора схемой | Тип есть, проверки нет — это ложь компилятору, и первая же смена контракта API даёт undefined в проде | |
Считать, что strict защищает от any, приехавшего из библиотечных типов | res.json() объявлен как any — это явный any, и noImplicitAny на него принципиально не срабатывает | |
Брать any на границе данных вместо unknown | any присваивается в обе стороны, и каждая операция над ним снова даёт any — дыра расползается по всему графу вывода | |
Ждать, что interface можно проверить через instanceof или typeof | Ни interface, ни type не доживают до рантайма — проверять там нечего | |
Пытаться сделать new T() или T.name из generic-параметра | Параметр стёрт; передавайте конструктор значением: ctor: new () => T | |
| Ждать, что список перегрузок диспетчеризуется во время выполнения | В выводе остаётся одна функция — разбор аргументов пишется руками внутри реализации | |
Считать type UserId = string отдельным типом | Псевдоним не создаёт нового типа: OrderId и любая голая строка подойдут молча | |
Считать, что implements связывает типы | implements — только проверка формы класса; совместимость всё равно определяется структурно | |
Аннотировать конфиг (const c: Config = {…}) и ждать точных ключей | Аннотация расширяет значение до объявленного типа — литеральные ключи и значения теряются | |
Заменять аннотацию на as Config, чтобы «сохранить тип» | as ничего не проверяет: опечатка в имени ключа пройдёт молча | |
Датировать satisfies версией 5.x | Оператор появился в TypeScript 4.9 | |
Ждать от as const заморозки во время выполнения | Это компиляторный readonly; мутацию запрещает Object.freeze, а через алиас типа T[] объект по-прежнему изменяем | |
Считать readonly защитой от записи по другой ссылке | readonly стирается — тот же объект под не-readonly типом пишется свободно | |
Считать, что arr[10] имеет тип `T \ | undefined` | По умолчанию нет; это включается флагом noUncheckedIndexedAccess, которого нет в strict |
Считать, что strictFunctionTypes делает контравариантными параметры методов | Параметры метода-сокращения остаются бивариантными; контравариантны только свойства-функции | |
| Объяснять несостоятельность TypeScript как недоработку | Это записанный компромисс между строгостью и продуктивностью — так его и надо защищать на собеседовании |
Значение для собеседований
Эта тема — главный фильтр домена. Интервьюер проверяет не знание синтаксиса, а модель исполнения: понимает ли кандидат, что между checker'ом и рантаймом нет ни одного канала связи. Один вопрос — «что делает as User во время выполнения?» — разводит людей на две группы: тех, кто отвечает «приводит тип», и тех, кто отвечает «ничего, он стёрт, и упасть он не может — поэтому валидация внешних данных обязана быть отдельным механизмом». Вторая половина разговора почти всегда уходит в несостоятельность: назовите дыру, объясните, почему её оставили, и скажите, чем вы её закрываете.
Что обычно проверяют:
- Что именно исчезает при компиляции, а что остаётся значением (
class,enum,namespace). - Что делает
asво время выполнения и как отличить законное применение от лжи компилятору. - Разницу тройки «аннотация /
as/satisfies» на одном и том же объекте конфигурации. - Что конкретно меняет
as constи почему без него ломается размеченное объединение. - Почему
type UserId = stringне защищает ничего и как это чинится брендом. - Чем
unknownотличается отanyи почему он — правильный тип дляJSON.parseиcatch. - Конкретный пример несостоятельности с причиной: ковариантность массивов, бивариантность параметров метода, непроверяемый индексный доступ.
- Как из стёртых типов всё-таки получить информацию о типе в рантайме (дискриминант,
class, схема).
Типичный неверный ответ: «as — это приведение типа, как cast в Java: если тип не совпадёт, будет ошибка». Из этой одной ошибки вырастает почти вся практика, которую придётся отлаживать: await res.json() as User объявляется проверкой, any затыкается новым as, а несовпадение контракта с бэкендом всплывает через три слоя как Cannot read properties of undefined. Второй классический провал — защищать TypeScript фразой «если компилируется, значит не упадёт». Компилируется — значит, checker не нашёл противоречий в тех правилах, которые он согласился проверять; массив всё ещё ковариантен, индекс не проверен, а данные из сети никто не смотрел.