Сужение типов это когда TypeScript определяет более конкретный тип значения в определённой точке кода на основе проверок, которые код уже сделал. Параметр string | number становится string внутри if (typeof x === "string") и number в else.
Вызов value.toFixed(2) до проверки был бы ошибкой компиляции, потому что у string нет toFixed. Проверка это обычный JavaScript, который выполняется во время работы программы; сужение это компилятор, который её читает и подстраивает тип. Ничего лишнего не генерируется.
Анализ потока управления
TypeScript проходит по каждому пути через функцию: if/else, ранние return и throw, switch, циклы и операторы с коротким замыканием &&, ||, ?? и ?:. В каждой точке тип переменной это то, что там ещё возможно.
Стиль с ранним возвратом («guard clauses») самый читаемый способ сужать: сначала обработайте особые случаи, и остальная функция работает с чистым типом.
Все способы сужения
| Форма | Пример | Сужает |
|---|---|---|
| typeof | typeof x === "string" | примитивы и функции |
| Истинность | if (x) | убирает null, undefined и ложные литералы |
| Равенство | x === "a", x == null, x !== undefined | литералы, null, undefined |
in | "swim" in pet | объединения объектов, по свойству |
| instanceof | err instanceof TypeError | экземпляры классов |
Array.isArray | Array.isArray(x) | массивы против всего остального |
| Присваивание | x = 5 | до присвоенного типа |
| Предикат типа | function isUser(x: unknown): x is User | всё, что можно проверить |
| Функция утверждения | function assertUser(x: unknown): asserts x is User | всё после вызова |
| Свойство-дискриминант | switch (shape.kind) | размеченные объединения |
Последние три разобраны на страницах о защитниках типов и размеченных объединениях. Остальные ниже.
Сужение по истинности
if (x) убирает null и undefined (а также литеральные типы false, 0, ""). Это коротко, но есть классическая ловушка: 0 и "" ложны, поэтому корректные значения принимаются за отсутствующие.
Для чисел и строк сравнивайте с undefined или null явно (или используйте ??). Для объектов, массивов и функций, которые никогда не бывают ложными, проверка истинности подходит.
Сужение через равенство
===, !==, == и != сужают обе стороны. Сравнение с литералом сужает до этого литерала; == null (нестрогое равенство) одной проверкой совпадает и с null, и с undefined, и это единственное место, где нестрогое равенство считается идиоматичным.
Сравнение двух переменных сужает обе до того, что у них может быть общего: если a: string | number и b: string | boolean проходят a === b, внутри if обе имеют тип string.
Оператор in
"key" in obj сужает объединение объектных типов до членов, у которых это свойство есть (или может быть).
in работает и с unknown, когда известно, что это объект: после typeof v === "object" && v !== null && "id" in v TypeScript знает, что у v есть свойство id типа unknown. Для объединений, которые вы проектируете сами, общее свойство-метка (kind: "fish") понятнее, чем проверка наличия методов; такой приём называется размеченным объединением.
Сужение при присваивании
У переменной есть объявленный тип и суженный тип, который следует за её присваиваниями. Присвоение значения сужает её до типа этого значения, в пределах объявленного типа.
Где сужение теряется
Сужение локально и консервативно. Несколько ситуаций его сбрасывают:
- Другое выражение. Проверка
obj.nameсужаетobj.name(иobj["name"]), но неobj[key], когдаkeyэто переменная типаstring, а не литерал, и не копию, сделанную до проверки. - Колбэки и переприсваивание. Внутри колбэка суженная
letсохраняет сужение, только если ей ничего не присваивается после создания колбэка.constили параметр, который никогда не переприсваивается, остаются суженными. - Проверки, спрятанные во вспомогательных функциях. Функция
isString(x: unknown): booleanничего не сообщает компилятору. Дайте ей возвращаемый тип-предикат,x is string, и её вызовы будут сужать так же, какtypeof.
Компилятор сообщает index.ts(5,38): error TS18048: 'x' is possibly 'undefined'. Колбэк может выполниться позже, после x = undefined. Удалите это последнее присваивание (или скопируйте значение в const внутри if), и код скомпилируется и дважды напечатает 5.
Вспомогательную функцию, которая возвращает boolean, можно исправить, объявив, что она доказывает. Это и есть защитник типа:
function isString(value: unknown): value is string {
return typeof value === "string";
}
Начиная с TypeScript 5.5 компилятор выводит такие предикаты для простых стрелочных функций, поэтому list.filter((x) => x !== undefined) теперь возвращает массив без undefined.
Часто задаваемые вопросы
Что такое сужение типов в TypeScript?
Сужение это уточнение типа переменной внутри блока на основе проверки, которую выполняет код. После if (typeof x === "string") тип string | number внутри if становится просто string, а в else просто number. Компилятор следит за if, else, return, switch, &&, || и ?:, чтобы определить тип в каждой точке; это называется анализом потока управления.
Почему TypeScript не сужает мой тип?
Частые причины: проверяется не то выражение, которое используется (проверено obj.a, а используется obj[key] с key типа string); значение это let, которое переприсваивается после создания колбэка, поэтому колбэк теряет сужение; или проверка спрятана во вспомогательной функции, которая возвращает обычный boolean, а не предикат типа x is T.
Работает ли сужение типов во время выполнения?
Проверки работают: typeof, instanceof, in и === это обычный выполняемый JavaScript. Само сужение существует только во время компиляции. TypeScript читает ваши проверки и подгоняет под них статические типы, а в сгенерированный JavaScript ничего не добавляется.
Как сузить тип unknown в TypeScript?
Теми же проверками: typeof value === "string", Array.isArray(value), value instanceof Date, а для объектов typeof value === "object" && value !== null && "id" in value. Для переиспользуемых проверок напишите функцию-защитник типа с возвращаемым типом value is T.
Как отфильтровать undefined из массива в TypeScript?
Начиная с TypeScript 5.5 items.filter((x) => x !== undefined) возвращает T[] без undefined, потому что компилятор выводит колбэк как предикат типа. В более старых версиях напишите предикат сами: items.filter((x): x is T => x !== undefined).