Обновлено: июль 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.

ОНЛАЙН-ПРАКТИКУМ
ЗАПУСК нейросети DEEPSEEK R1 ЛОКАЛЬНО НА СВОЕМ КОМПЬЮТЕРЕ
ЧТО БУДЕТ НА ОБУЧЕНИИ?
  • ПОКАЖЕМ, КАК РАЗВЕРНУТЬ МОДЕЛЬ нейросети DEEPSEEK R1 ПРЯМО НА СВОЁМ КОМПЬЮТЕРЕ
  • Где и как применять? Потестируем модель после установки на разных задачах
  • Как дообучить модель под себя?

Встроенные 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 добавят новый член.

Большой практикум
ЗАМЕНИ ВСЕ НЕЙРОСЕТИ НА ОДНУ — PERPLEXITY
ПОКАЖЕМ НА КОНКРЕТНЫХ КЕЙСАХ
  • Освой нейросеть Perplexity и узнай, как пользоваться функционалом остальных ИИ в одном
  • УЧАСТВОВАТЬ ЗА 0 РУБ.
  • Расскажем, как получить подписку
Участвовать бесплатно
ОНЛАЙН-ПРАКТИКУМ
ЗАПУСК нейросети DEEPSEEK R1 ЛОКАЛЬНО НА СВОЕМ КОМПЬЮТЕРЕ
ЧТО БУДЕТ НА ОБУЧЕНИИ?
  • ПОКАЖЕМ, КАК РАЗВЕРНУТЬ МОДЕЛЬ нейросеть DEEPSEEK R1 ПРЯМО НА СВОЁМ КОМПЬЮТЕРЕ
Участвовать бесплатно