Menu

Сужение типов в TypeScript: анализ потока управления

Сужение это то, как TypeScript превращает широкий тип вроде string | number в конкретный внутри if, switch или после раннего return. Все формы сужения в одном месте: typeof, истинность, равенство, in, instanceof, присваивания, предикаты типов и размеченные объединения.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Сужение типов это когда TypeScript определяет более конкретный тип значения в определённой точке кода на основе проверок, которые код уже сделал. Параметр string | number становится string внутри if (typeof x === "string") и number в else.

Вызов value.toFixed(2) до проверки был бы ошибкой компиляции, потому что у string нет toFixed. Проверка это обычный JavaScript, который выполняется во время работы программы; сужение это компилятор, который её читает и подстраивает тип. Ничего лишнего не генерируется.

Анализ потока управления

TypeScript проходит по каждому пути через функцию: if/else, ранние return и throw, switch, циклы и операторы с коротким замыканием &&, ||, ?? и ?:. В каждой точке тип переменной это то, что там ещё возможно.

Стиль с ранним возвратом («guard clauses») самый читаемый способ сужать: сначала обработайте особые случаи, и остальная функция работает с чистым типом.

Все способы сужения

ФормаПримерСужает
typeoftypeof x === "string"примитивы и функции
Истинностьif (x)убирает null, undefined и ложные литералы
Равенствоx === "a", x == null, x !== undefinedлитералы, null, undefined
in"swim" in petобъединения объектов, по свойству
instanceoferr instanceof TypeErrorэкземпляры классов
Array.isArrayArray.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).

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ