Содержание
  1. Что такое type guard и зачем сужать тип
  2. Встроенные type guard’ы
  3. Какой type guard выбрать: сравнение
  4. Пользовательские type guard: предикаты arg is Type
  5. Размеченные объединения и проверка на исчерпывающесть
  6. Функции-утверждения: asserts
  7. Практический пример: разбор ответа API
  8. Частые вопросы
СправочникОбновлено · 07.2026

Понимание TypeScript: экземпляр Type Guard

10 июня 2024 · 8 минут чтения

Обновлено: июль 2026. Все примеры проверены на актуальной версии компилятора TypeScript.

Type guard (защитник типа) — это проверка, после которой TypeScript сужает тип переменной внутри блока кода. Пишете typeof x === "string" — и в ветке if компилятор считает x строкой, разрешая x.toUpperCase(). Сам процесс уточнения типа называется сужением (narrowing).

function len(x: string | string[]): number {
  if (typeof x === "string") {
    return x.length;      // x: string
  }
  return x.length;        // x: string[]
}

Коротко, что нужно знать:

  • Встроенные guard’ы: typeof, instanceof, in и проверка на истинность — покрывают большинство случаев.
  • Свой guard задаётся предикатом arg is Type — функция возвращает boolean, а компилятор трактует её как проверку типа.
  • Размеченные объединения (discriminated unions) сужают тип по общему полю-дискриминанту.
  • Функции с asserts сужают тип не через ветвление, а через факт, что вызов не бросил исключение.
  • Тип never в default даёт проверку на исчерпывающесть — компилятор напомнит про необработанный вариант.

Что такое type guard и зачем сужать тип

Переменная типа string | number — это объединение (union). Пока TypeScript не знает, какой из вариантов перед ним сейчас, он запрещает операции, доступные лишь одному из типов: у number нет .toUpperCase(), у string нет умножения. Type guard снимает эту неопределённость: проверка в рантайме сообщает компилятору, с каким конкретно типом он работает в данной ветке.

Механизм называется анализом потока управления (control flow analysis). Компилятор читает код сверху вниз, отслеживает каждую проверку и на её основе пересчитывает тип переменной для каждой ветки. Поэтому в примере ниже во втором return тип уже сужен без единого приведения:

function padLeft(padding: number | string, input: string): string {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;   // padding: number
  }
  return padding + input;                 // padding: string
}

Мини-вывод. Type guard не меняет значение в рантайме — он меняет представление компилятора о типе. Правильно расставленные guard’ы убирают приведения as и ловят ошибки на этапе сборки. Полный перечень механизмов сужения — в официальном разделе TypeScript Handbook: Narrowing.

Встроенные type guard’ы

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

Оператор typeof возвращает одну из строк: "string", "number", "bigint", "boolean", "symbol", "undefined", "object" или "function". Подходит для примитивных типов. Важная ловушка из JavaScript: typeof null возвращает "object" — это историческая ошибка языка, из-за неё нельзя отличить null от объекта одним лишь typeof.

Проверка на истинность

Условие if (value) отсекает все ложные значения: 0, NaN, "", 0n, null и undefined. Удобно для отсечения null перед работой с объектом, но помните: пустая строка и ноль тоже ложны, и такая проверка их выбросит вместе с null.

function printAll(strs: string | string[] | null) {
  if (strs && typeof strs === "object") {
    for (const s of strs) console.log(s);   // strs: string[]
  }
}

Сравнение (equality)

Операторы ===, !==, == и != тоже сужают тип. Отдельно полезен нестрогий != null: он одним условием убирает и null, и undefined.

function multiply(value: number | null | undefined, factor: number) {
  if (value != null) {
    return value * factor;   // value: number
  }
  return 0;
}

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

Оператор in проверяет, есть ли у объекта свойство с указанным именем, и сужает объединение по этому признаку:

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) return animal.swim();   // animal: Fish
  return animal.fly();                          // animal: Bird
}

Нюанс: если свойство опциональное (swim?), тип с ним попадёт в обе ветки условия — in надёжен только для обязательных свойств.

instanceof — для экземпляров классов

Когда типы заданы классами, работает instanceof: он проверяет, находится ли объект в цепочке прототипов класса. Это классический «экземплярный» type guard.

class Animal {
  makeSound() { console.log("Some generic sound"); }
}
class Dog extends Animal {
  makeSound() { console.log("Bark"); }
}
class Cat extends Animal {
  makeSound() { console.log("Meow"); }
}

function makeAnimalSound(animal: Animal) {
  if (animal instanceof Dog) {
    animal.makeSound();        // animal: Dog -> Bark
  } else if (animal instanceof Cat) {
    animal.makeSound();        // animal: Cat -> Meow
  } else {
    animal.makeSound();        // animal: Animal
  }
}

Мини-вывод. typeof — для примитивов, instanceof — для экземпляров классов, in — для объектных литералов без классов. Проверка на истинность и сравнение отсекают null и undefined.

Какой type guard выбрать: сравнение

Как выбрать type guard: от типа данных к нужной проверке
Как выбрать type guard: от типа данных к нужной проверке
Guard Что проверяет Когда применять Ограничение
typeof Примитивный тип значения string / number / boolean / symbol / bigint typeof null === "object"
истинность Truthy / falsy быстро отсечь null и undefined выбрасывает 0, "", NaN
=== / != null Равенство значений литералы, отсечение null+undefined только по значению
in Наличие свойства объединения объектных литералов опциональные свойства в обеих ветках
instanceof Цепочку прототипов объекты, созданные через классы не работает для интерфейсов и литералов
предикат arg is T Произвольную логику сложная проверка, переиспользование корректность на совести автора
дискриминант kind Литеральное поле-метку размеченные объединения нужно завести общее поле

Пользовательские type guard: предикаты arg is Type

Встроенных проверок хватает не всегда. Свой guard объявляется через предикат типа — в сигнатуре функции вместо boolean указывается parameterName is Type. Если функция вернула true, компилятор сужает тип аргумента до указанного:

interface Bird { fly(): void; }
interface Fish { swim(): void; }

function isBird(pet: Bird | Fish): pet is Bird {
  return (pet as Bird).fly !== undefined;
}

function move(pet: Bird | Fish) {
  if (isBird(pet)) {
    pet.fly();    // pet: Bird
  } else {
    pet.swim();   // pet: Fish
  }
}

Предикаты работают и при фильтрации массивов — filter с guard’ом возвращает сужённый тип элементов, а не исходное объединение:

const zoo: (Bird | Fish)[] = getAnimals();
const birds: Bird[] = zoo.filter(isBird);   // тип сохранён

Ответственность за корректность здесь на разработчике: если тело функции вернёт true не для того типа, компилятор поверит на слово и ошибка проявится в рантайме.

Размеченные объединения и проверка на исчерпывающесть

Размеченное объединение (discriminated union) — это когда у всех членов union есть общее поле с литеральным типом-меткой. По нему switch сужает тип автоматически, без instanceof и приведений:

interface Circle { kind: "circle"; radius: number; }
interface Square { kind: "square"; sideLength: number; }
type Shape = Circle | Square;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle": return Math.PI * shape.radius ** 2;   // shape: Circle
    case "square": return shape.sideLength ** 2;         // shape: Square
    default:
      const _exhaustive: never = shape;   // страховка на будущее
      return _exhaustive;
  }
}

Поле kind здесь — дискриминант. Приём с never в default — это проверка на исчерпывающесть. В never нельзя присвоить никакой тип, кроме самого never. Пока обработаны все варианты, shape в default имеет тип never и присваивание проходит. Добавьте в union новый вариант Triangle — и компилятор тут же подсветит ошибку «Type ‘Triangle’ is not assignable to type ‘never’», напоминая дописать ветку. Это дешёвая страховка от забытого случая при росте кода.

Мини-вывод. Дискриминант удобнее instanceof, когда данные приходят как обычные объекты (например из JSON) — классов там нет, а поле-метка есть.

Функции-утверждения: asserts

Начиная с TypeScript 3.7 есть ещё один способ сужения — сигнатуры-утверждения (assertion signatures). Такая функция не возвращает boolean, а бросает исключение, если условие не выполнено. Сам факт, что вызов не прервал выполнение, сужает тип для всего оставшегося кода. Форм две.

asserts condition — гарантирует, что условие истинно дальше по коду:

function assert(condition: any, msg?: string): asserts condition {
  if (!condition) throw new Error(msg);
}

function yell(str: unknown) {
  assert(typeof str === "string");
  return str.toUpperCase();   // str: string
}

asserts val is Type — утверждает конкретный тип аргумента:

function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
  if (val === undefined || val === null) {
    throw new Error(`Expected value to be defined, got ${val}`);
  }
}

В отличие от предиката is, который сужает тип только внутри своей ветки, asserts-функция сужает тип линейно — от строки вызова и до конца области видимости. Удобно для входной валидации, где невалидные данные должны сразу прерывать выполнение.

Практический пример: разбор ответа API

Данные из сети приходят как unknown или any — доверять их форме нельзя. Пользовательский guard проверяет структуру один раз и дальше отдаёт типизированное значение:

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

function isUser(data: unknown): data is User {
  return (
    typeof data === "object" &&
    data !== null &&
    "id" in data && typeof (data as any).id === "number" &&
    "name" in data && typeof (data as any).name === "string"
  );
}

async function loadUser(url: string): Promise<User | null> {
  const data: unknown = await fetch(url).then((r) => r.json());
  if (isUser(data)) {
    return data;   // data: User — можно обращаться к полям безопасно
  }
  console.warn("Ответ API не соответствует типу User");
  return null;
}

Здесь guard закрывает разрыв между «что обещает тип» и «что реально пришло». Без такой проверки одно приведение as User замаскировало бы битый ответ и уронило бы код при первом обращении к полю.

Частые вопросы

Чем type guard отличается от приведения типа (as)?

Приведение as — это обещание компилятору «поверь, тут такой тип», без проверки в рантайме. Type guard реально проверяет значение во время выполнения, поэтому безопаснее: неверные данные будут пойманы, а не замаскированы.

Работает ли instanceof с интерфейсами?

Нет. Интерфейсы и типы-литералы стираются при компиляции, в рантайме их нет. instanceof опирается на прототип класса, поэтому применим только к объектам, созданным через class. Для интерфейсов используйте in или пользовательский предикат.

Что такое narrowing простыми словами?

Это когда компилятор из широкого типа (например string | number) выводит более узкий (string) на основании ваших проверок. Type guard — инструмент, который запускает сужение.

Когда брать discriminated union вместо instanceof?

Когда данные — это обычные объекты, а не экземпляры классов: ответы API, состояния редьюсера, конфиги. Общее поле-метка (kind, type, status) даёт сужение без классов и вдобавок проверку на исчерпывающесть через never.

Зачем нужен тип never в проверке?

never в default-ветке заставляет компилятор ругаться, если в объединении появился необработанный вариант. Это защищает от забытых случаев, когда через полгода в union добавят новый член.

Читайте также

3 материала