← назад к разделу

Часто переменная может содержать значение одного из нескольких типов: строку или число, объект или null, успех или ошибку. Сужение типов (type narrowing) — это то, как TypeScript внутри блока кода понимает, какой именно тип сейчас перед нами, и разрешает безопасно с ним работать.

Зачем сужать типы

Допустим, функция принимает значение, которое может быть строкой или числом:

function printId(id: string | number) {
  // id.toUpperCase() — ошибка: у number нет toUpperCase
  console.log(id);
}

TypeScript не даст вызвать id.toUpperCase(), потому что id может оказаться числом. Чтобы получить доступ к строковым методам, нужно сначала доказать компилятору, что в этой ветке id — точно строка. Это и есть сужение: проверка во время выполнения, которую TypeScript умеет читать и применять к типам.

Короткая формула: проверил значение условием — внутри ветки тип сузился.

typeof — для примитивов

Оператор typeof возвращает строку с названием типа значения. TypeScript понимает такие проверки и сужает тип в каждой ветке:

function printId(id: string | number) {
  if (typeof id === "string") {
    // здесь id: string — методы строки доступны
    console.log(id.toUpperCase());
  } else {
    // здесь id: number
    console.log(id.toFixed(2));
  }
}

typeof различает примитивы: "string", "number", "boolean", "bigint", "symbol", "undefined", "function" и "object". Важная ловушка: typeof null === "object" — для отсева null нужна отдельная проверка (см. ниже).

instanceof — для классов

Когда значение может быть экземпляром класса, помогает instanceof. Он проверяет цепочку прототипов и тоже сужает тип:

function describe(value: Date | string) {
  if (value instanceof Date) {
    // здесь value: Date
    return value.toISOString();
  }
  // здесь value: string
  return value.trim();
}

instanceof работает с тем, что создаётся через new: встроенные Date, Error, Map, а также ваши собственные классы.

Оператор in — по наличию свойства

Если типы различаются набором полей, можно проверять наличие свойства оператором in:

interface Cat { meow(): void }
interface Dog { bark(): void }

function speak(animal: Cat | Dog) {
  if ("meow" in animal) {
    // здесь animal: Cat
    animal.meow();
  } else {
    // здесь animal: Dog
    animal.bark();
  }
}

Это удобно, когда у вас интерфейсы без общего класса — instanceof к ним неприменим, а in подходит.

Проверки на null и undefined

Значения null и undefined — частый источник ошибок. TypeScript сужает тип и здесь:

function greet(name: string | null) {
  if (name === null) {
    return "Привет, гость";
  }
  // здесь name: string
  return `Привет, ${name.toUpperCase()}`;
}

Простая проверка на «истинность» отсекает сразу и null, и undefined, и пустую строку:

function length(text?: string) {
  if (!text) {
    return 0; // text здесь undefined или пустая строка
  }
  return text.length; // text: string
}

Удобный приём — раннее возвращение (early return): отсеяли «пустой» случай в начале, а дальше работаем с гарантированно валидным значением.

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

Иногда проверка сложнее одного оператора, и хочется вынести её в отдельную функцию. Чтобы TypeScript продолжал сужать тип после такой функции, она должна возвращать type predicate — выражение вида параметр is Тип:

interface User {
  id: number;
  email: string;
}

// функция-guard: возвращает not просто boolean, а "value is User"
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "email" in value
  );
}

function handle(payload: unknown) {
  if (isUser(payload)) {
    // здесь payload: User
    console.log(payload.email);
  }
}

Запись value is User говорит компилятору: «если эта функция вернула true, считай аргумент типом User». Так вы переиспользуете нетривиальную проверку и сохраняете сужение.

Discriminated unions

Самый надёжный приём для объектов — размеченное объединение (discriminated union). У каждого варианта есть общее поле-метка (литеральный тип), по которому TypeScript однозначно различает варианты:

type Result =
  | { status: "ok"; data: string }
  | { status: "error"; message: string };

function render(result: Result) {
  switch (result.status) {
    case "ok":
      // здесь result: { status: "ok"; data: string }
      return result.data;
    case "error":
      // здесь result: { status: "error"; message: string }
      return result.message;
  }
}

Поле status здесь — дискриминант. Проверив его, TypeScript сразу знает, какие ещё поля доступны: в ветке "ok" есть data, в ветке "error"message. Это безопаснее, чем проверять наличие полей по одному.

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

function render(result: Result): string {
  switch (result.status) {
    case "ok":
      return result.data;
    case "error":
      return result.message;
    default:
      // если все варианты разобраны, сюда попадает never
      const exhaustive: never = result;
      return exhaustive;
  }
}

Работа с unknown

Тип unknown — это «безопасный any». Он означает «значение есть, но тип неизвестен», и TypeScript запрещает любые операции с ним, пока тип не сужен. Это и делает его правильным выбором для данных извне: ответ сети, разбор JSON, ввод пользователя.

function parse(json: string) {
  const data: unknown = JSON.parse(json); // JSON.parse возвращает any, фиксируем как unknown

  // data.id — ошибка: тип неизвестен
  if (isUser(data)) {
    console.log(data.id); // здесь data: User
  }
}

В отличие от any, unknown заставляет вас сначала проверить значение — а значит, ошибка типа всплывёт при компиляции, а не во время выполнения. Короткая формула: данные извне принимай как unknown и сужай guard'ом перед использованием.

Почему сужение делает код безопасным

Каждая проверка-сужение работает на двух уровнях сразу. Во время выполнения это обычное условие. Во время компиляции TypeScript использует ту же проверку, чтобы запретить недопустимые операции в каждой ветке. Поэтому «у undefined нет такого метода» и подобные сбои ловятся ещё до запуска: компилятор просто не даст обратиться к полю, которого в этой ветке может не быть.

Коротко

  • Сужение типов — это то, как TypeScript внутри ветки понимает конкретный тип значения из объединения.
  • typeof сужает примитивы, instanceof — экземпляры классов, in — по наличию свойства, сравнение с null/undefined — отсекает «пустые» значения.
  • Помни ловушку typeof null === "object" — для null нужна отдельная проверка.
  • Пользовательский type guard с возвратом value is Тип переиспользует сложную проверку и сохраняет сужение.
  • Discriminated union с полем-дискриминантом — самый надёжный способ различать варианты объектов; приём с never даёт проверку полноты.
  • Данные извне принимай как unknown и сужай guard'ом — компилятор заставит проверить тип до использования.

Что почитать дальше

  • Основы системы типов — объединения, литеральные типы и из чего складываются типы, которые мы сужаем.
  • Generics — обобщённые типы, которые часто комбинируют с type guards.
  • Асинхронность и event loop — где unknown особенно полезен при разборе ответов сети.