Урок по треку TypeScript 6 мин

Объединения и сужение типов

Union, typeof, in, instanceof, type guards и дискриминированные объединения.

Объединение A | B значит «значение одного из типов». Пока TypeScript не знает, какого именно, доступны только общие для всех членов операции. Сужение (narrowing) это анализ кода, после которого компилятор в конкретной ветке знает тип точнее.

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

function show(x: string | number | null) {
  if (x === null) return;              // проверка равенства
  if (typeof x === 'string') {         // typeof
    x.toUpperCase();                   // x: string
  } else {
    x.toFixed(2);                      // x: number
  }
}
  • typeof различает 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function'. Помните: typeof null === 'object'.
  • Проверка на истинность (if (x)) убирает null и undefined, но заодно отсекает '', 0, NaN, false. Частый баг с числами.
  • instanceof сужает до класса: if (e instanceof Error) e.message.
  • in проверяет наличие свойства: if ('swim' in animal).
  • Array.isArray(x) сужает до массива.
  • Присваивание тоже сужает: после let v: string | number = 'a' переменная до следующего присваивания считается string.

Дискриминированные объединения

Самый надёжный приём: у каждого варианта есть общее поле с литеральным типом (тег).

type Result =
  | { status: 'ok'; data: string }
  | { status: 'error'; error: Error };

function handle(r: Result) {
  switch (r.status) {
    case 'ok': return r.data;          // r: { status: 'ok'; data: string }
    case 'error': throw r.error;
  }
}

Так моделируют состояния загрузки, экшены редьюсера, события.

Исчерпывающая проверка через never

Если в default присвоить значение переменной типа never, компилятор упадёт, когда кто-то добавит новый вариант и забудет его обработать:

default: {
  const _never: never = r;
  throw new Error('unreachable');
}

Пользовательские type guards

Функция с возвращаемым типом x is T сообщает компилятору результат проверки:

function isUser(v: unknown): v is User {
  return typeof v === 'object' && v !== null && 'id' in v;
}

Компилятор верит guard'у на слово: если проверка внутри неверна, типы будут врать. С TypeScript 5.5 простые стрелки вроде x => x !== null выводятся как предикаты автоматически, поэтому arr.filter(x => x !== null) уже даёт массив без null.

Функция-утверждение asserts v is T сужает тип после вызова, а при неудаче должна бросить исключение.

Где сужение теряется

  • В колбэке: сужение переменной let переносится внутрь замыкания, только если ниже ей больше ничего не присваивают (так стало с TypeScript 5.4). Иначе к моменту вызова значение могло измениться, и сужение теряется. С const и параметрами без переприсваивания работает всегда.
  • После вызова функции сужение свойств объекта (obj.x) сохраняется, хотя функция могла их поменять. Это осознанный компромисс.
  • as не сужает, а заставляет: проверки нет.

Частые ошибки

  • if (count) для number | undefined: ноль уходит в ветку «нет значения».
  • Тег не литеральный (status: string): дискриминации не будет.
  • as User вместо проверки данных извне.

Что спросят на собесе

  • Какие способы сужения знаете и чем in отличается от instanceof.
  • Что такое дискриминированное объединение и зачем тег.
  • Как сделать исчерпывающую проверку switch.
  • Что такое type predicate x is T и чем опасен.
  • Почему as не равно проверке.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.

Вопросы модуля (46)