Система типов
Чем на самом деле являются типы в TypeScript — стирание на этапе компиляции, assertion против satisfies, as const, брендированные типы вместо номинальной типизации, как any отравляет вывод типов и где система типов намеренно несостоятельна.
13 вопросов
JuniorТеорияОчень частоКакие конструкции TypeScript доживают до порождённого JavaScript, а какие исчезают?
Какие конструкции TypeScript доживают до порождённого JavaScript, а какие исчезают?
Всё, что живёт на уровне типов, стирается: аннотации, interface, псевдонимы type, generics и as исчезают, а на выходе получается обычный JavaScript с неизменной семантикой. Код порождают только конструкции уровня значений — class, enum, namespace.
Типичные ошибки
- ✗Ждать, что неверный ответ API будет отвергнут во время выполнения по объявленному типу
- ✗Пытаться добраться до параметра generic
Tиз тела функции во время выполнения - ✗Считать, что
strictдобавляет проверки в рантайме, а не при компиляции
Уточняющие вопросы
- →Почему
enumпорождает код, а псевдонимtype— нет? - →Как проверить форму ответа API, если типы стираются?
JuniorТеорияОчень частоЧто делает as во время выполнения, и как он может провалиться?
Что делает as во время выполнения, и как он может провалиться?
Ничего, и провалиться он не может. as стирается: он лишь велит компилятору считать выражение другим типом, не выполняя ни преобразования, ни проверки. Поэтому await res.json() as User — это утверждение о данных, а не их проверка.
Типичные ошибки
- ✗Считать
asприведением, которое преобразует или проверяет значение - ✗Доверять
res.json() as Userтак, будто данные были проверены - ✗Тянуться к
as, чтобы заглушить ошибку, вместо сужения настоящей проверкой
Уточняющие вопросы
- →Почему компилятор отвергает
asмежду двумя совсем не связанными типами? - →Что вы напишете вместо
as, чтобы действительно проверить данные?
JuniorТеорияЧастоЧто перестаёт проверяться, как только у значения тип any?
Что перестаёт проверяться, как только у значения тип any?
Всё, что с ним связано. any присваивается в обе стороны, поэтому у значения можно прочитать любое свойство, вызвать его, проиндексировать и передать куда угодно без жалоб. Каждая такая операция снова даёт any, поэтому он расползается дальше.
Типичные ошибки
- ✗Полагать, что чтение свойства у
anyдаётunknown, а не сноваany - ✗Ждать, что
noImplicitAnyпоймаетany, который вы написали явно - ✗Считать, что типизированный параметр заново проверяет переданный в него
any
Уточняющие вопросы
- →Почему
unknownсодержит те же значения, что иany, но остаётся безопасным? - →Какой флаг компилятора покажет
any, просочившийся из библиотеки?
MiddleТеорияЧастоГде any действительно уместен, а где это дурной запах?
Где any действительно уместен, а где это дурной запах?
Он оправдан в позиции ограничения, невидимой вызывающему, — T extends (...args: any[]) => any — и во внутренней машинерии, где настоящий тип восстанавливается на выходе. На границе данных это дурной запах: unknown требует проверки, и проверки остаются.
Типичные ошибки
- ✗Типизировать внешние данные как
any, когдаunknownпринимает их и сохраняет проверки - ✗Считать
anyиunknownодинаковыми, раз они принимают одни и те же значения - ✗Разносить
anyпо внутренним помощникам там, где настоящий тип был доступен
Уточняющие вопросы
- →Почему
any[]допустим внутри ограничения generic, но не в публичной сигнатуре? - →Какой тип у пойманной ошибки
error, и что с ней следует делать?
MiddleТеорияЧастоЧто as const меняет в объектном литерале или массиве?
Что as const меняет в объектном литерале или массиве?
Он выключает расширение типа: свойства становятся readonly, каждый литерал сохраняет литеральный тип вместо расширения до string или number, а массив становится readonly-кортежем. Это и позволяет вывести union через typeof COLORS[number].
Типичные ошибки
- ✗Ждать, что
as constзаморозит объект во время выполнения - ✗Путать его с объявлением
const, которое фиксирует только привязку - ✗Забывать, что массив становится readonly-кортежем, а не изменяемым массивом
Уточняющие вопросы
- →Почему
typeof COLORS[number]даёт полезный union только послеas const? - →Что ломается при передаче массива с
as constв параметр типаstring[]?
JuniorТеорияИногдаКакую задачу решает брендированный (opaque) тип?
Какую задачу решает брендированный (opaque) тип?
TypeScript сравнивает типы по структуре, поэтому type UserId = string — это просто string: на месте UserId примут и OrderId, и любую строку. Бренд пересекает базовый тип с фантомным маркером, которого нет ни у одной обычной строки.
Типичные ошибки
- ✗Считать, что обычный псевдоним
typeуже создаёт тип, отличный от базового - ✗Ждать, что бренд существует настоящим свойством значения во время выполнения
- ✗Раздавать
asк брендированному типу вместо прохода через единственную фабрику
Уточняющие вопросы
- →Почему бренд ничего не стоит в порождённом JavaScript?
- →Что мешает вызывающему обойти фабрику через
as?
MiddleКодИногдаРеализуйте UserId, которому не удовлетворяет обычная string
Реализуйте UserId, которому не удовлетворяет обычная string
Пересеките базовый тип с фантомным полем по ключу unique symbol — тогда ни одна обычная string в него не присваивается. Откройте единственную фабрику, которая проверяет сырое значение и приводит его к бренду. Бренд живёт только в типах, порождённый JavaScript не меняется.
Типичные ошибки
- ✗Считать, что голый псевдоним
typeилиstring & {}уже исключает обычную строку - ✗Экспортировать бренд, позволяя вызывающим слепить
UserIdвручную черезas - ✗Тянуться к обёртке-class и платить выделением памяти за каждый идентификатор
Уточняющие вопросы
- →Зачем оставлять символ бренда неэкспортированным из модуля?
- →Как забрендировать идентификатор-
numberиOrderId, чтобы они не совпали?
MiddleТеорияИногдаTypeScript намеренно несостоятелен — назовите конкретный пример и причину
TypeScript намеренно несостоятелен — назовите конкретный пример и причину
Массивы ковариантны, поэтому Dog[] примут там, где ждут Animal[], и потом в него можно положить Cat. Параметры методов остаются бивариантными даже при strictFunctionTypes, а as и any попросту отключают проверку типов совсем.
Типичные ошибки
- ✗Считать, что
strictделает систему типов состоятельной - ✗Называть только
any, упуская дыры вариантности в массивах и методах - ✗Называть несостоятельность багом, а не намеренным компромиссом ради удобства
Уточняющие вопросы
- →Почему параметры методов бивариантны, а свойство-функция контравариантно?
- →Что сломалось бы в повседневном коде, стань массивы инвариантными?
MiddleТеорияИногдаЧто даёт satisfies такого, чего не дают аннотация и as?
Что даёт satisfies такого, чего не дают аннотация и as?
satisfies, появившийся в TypeScript 4.9, сверяет выражение с типом, сохраняя тип, выведенный из литерала. Аннотация сверяет, но расширяет тип до себя, теряя точные ключи и значения; as сохраняет тип, но не проверяет ничего.
Типичные ошибки
- ✗Считать, что
satisfiesвыполняет проверку значения во время выполнения - ✗Ждать, что аннотация сохранит литеральные ключи объекта
- ✗Тянуться к
asтам, где нужны и проверка, и выведенный тип
Уточняющие вопросы
- →Что даёт
keyof typeof cfgпослеsatisfiesиз того, что теряет аннотация? - →Почему при
satisfiesлишнее свойство всё равно остаётся ошибкой?
SeniorТеорияИногдаПочему сигнатуры перегрузки не диспетчеризуются в рантайме, как перегрузки в Java?
Почему сигнатуры перегрузки не диспетчеризуются в рантайме, как перегрузки в Java?
Потому что перегрузки — это типы, а типы стираются: в порождённом JavaScript остаётся одна функция. Список перегрузок лишь описывает проверяющему сигнатуры вызова; единственная реализация обязана принять все случаи и диспетчеризовать вручную.
Типичные ошибки
- ✗Ждать, что компилятор породит диспетчер из списка перегрузок
- ✗Писать тела для перегрузок вместо одной реализации, обрабатывающей все случаи
- ✗Полагать, что сигнатура реализации видна вызывающим
Уточняющие вопросы
- →Почему сигнатуру реализации нельзя вызвать снаружи?
- →Когда одна сигнатура с union яснее набора перегрузок?
SeniorДебаггингИногдаОпечатка в разобранных данных спокойно компилируется под strict — найдите причину
Опечатка в разобранных данных спокойно компилируется под strict — найдите причину
Идите по any вверх по цепочке. res.json() объявлен возвращающим any, поэтому data и всё выведенное из него — тоже any, и проверяющий перестаёт замечать опечатки и неверные аргументы. noImplicitAny не срабатывает: этот any явный.
Типичные ошибки
- ✗Ждать, что
noImplicitAnyпоймаетany, явно объявленный библиотекой - ✗Аннотировать
dataкакOrderи принимать эту аннотацию за проверку в рантайме - ✗Винить выключенный
strictвместо поиска места, где вошёлany
Уточняющие вопросы
- →Какое правило линтера показало бы этот
anyещё на ревью? - →Почему тип
unknownуdataнемедленно выводит ошибку на поверхность?
SeniorТеорияРедкоТипы стираются — как тогда получить информацию о типе во время выполнения?
Типы стираются — как тогда получить информацию о типе во время выполнения?
От системы типов вы её не получите — её воссоздают значениями. Держите поле-дискриминант в самих данных, используйте class, чей конструктор доживает до instanceof, или валидатор схемы, который проверяет в рантайме и выводит статический тип.
Типичные ошибки
- ✗Ждать, что параметр generic
Tможно изучить изнутри тела функции - ✗Считать, что
emitDecoratorMetadataзаписывает полные формы generic или interface - ✗Принимать аннотацию на границе за проверку данных во время выполнения
Уточняющие вопросы
- →Почему
classдаётinstanceof, аinterface— нет? - →Как валидатору схемы удаётся быть и проверкой, и источником типа одновременно?
SeniorДизайнРедкоОткрыв для себя satisfies, коллега присылает pull request, где заменяет им каждую аннотацию типа в кодовой базе — на типах возврата экспортируемых функций, на публичных interface, которые реализуют классы, и на объектах конфигурации, ради которых оператор и вводили. Аргумент такой: satisfies всегда проверяет не меньше аннотации и при этом ничего не теряет, поэтому аннотация стала попросту избыточной. Объясните, где это рассуждение верно, а где ломается, что даёт аннотация на публичной поверхности API из того, чего не даёт выведенный тип, и какую политику по аннотациям, satisfies и as вы запишете для команды.
Открыв для себя satisfies, коллега присылает pull request, где заменяет им каждую аннотацию типа в кодовой базе — на типах возврата экспортируемых функций, на публичных interface, которые реализуют классы, и на объектах конфигурации, ради которых оператор и вводили. Аргумент такой: satisfies всегда проверяет не меньше аннотации и при этом ничего не теряет, поэтому аннотация стала попросту избыточной. Объясните, где это рассуждение верно, а где ломается, что даёт аннотация на публичной поверхности API из того, чего не даёт выведенный тип, и какую политику по аннотациям, satisfies и as вы запишете для команды.
Берите satisfies там, где нужны и проверка, и точный выведенный тип: конфиги, таблицы маршрутов, данные с as const. Оставьте обычную аннотацию на публичной поверхности API, где объявленный тип и есть контракт, а расширение — это цель, а не потеря.
Типичные ошибки
- ✗Считать выведенный тип безопасной заменой объявленного публичного контракта
- ✗Полагать, что
satisfies— более слабая проверка, чем аннотация, а не просто другая - ✗Позволять
asподменять любую из них, ведь он не проверяет вообще ничего
Уточняющие вопросы
- →Как выведенный тип возврата экспортируемой функции расширяет радиус поражения рефакторинга?
- →Почему при
satisfiesконтроль лишних свойств всё равно остаётся в силе?