Type Guards

Type guards — механизмы сужения типов в рантайме: typeof, instanceof, in, пользовательские type guards (is) и assertion functions.

Зачем нужно

  • TypeScript не может автоматически определить конкретный тип из union — нужна проверка в рантайме
  • Type guards позволяют безопасно работать с union-типами
  • Пользовательские type guards инкапсулируют сложную логику проверки
  • Assertion functions гарантируют тип или выбрасывают исключение

Где используется

  • Обработка API-ответов (данные или ошибка)
  • Работа с union-типами (string | number, User | null)
  • Валидация входных данных
  • Discriminated unions в switch/case

Предпосылки

typeof guard

Для примитивных типов: string, number, boolean, symbol, bigint, undefined, function, object:

function format(value: string | number): string {
  if (typeof value === "string") {
    // TypeScript сужает: value: string
    return value.toUpperCase();
  }
  // TypeScript сужает: value: number
  return value.toFixed(2);
}

// typeof в тернарном операторе
function double(value: string | number): string | number {
  return typeof value === "string" ? value.repeat(2) : value * 2;
}

// Множественные typeof
function process(value: string | number | boolean | null): string {
  if (typeof value === "string") return `String: ${value}`;
  if (typeof value === "number") return `Number: ${value}`;
  if (typeof value === "boolean") return `Bool: ${value}`;
  return "null";
}

Ограничения typeof

// typeof null === "object" — известная особенность JS
function handle(value: object | null) {
  if (typeof value === "object") {
    // value: object | null — null НЕ отфильтрован!
    // Нужна дополнительная проверка:
    if (value !== null) {
      // value: object
    }
  }
}

// typeof для массивов тоже "object"
function isArray(value: unknown): value is unknown {
  return Array.isArray(value); // Используйте Array.isArray
}

instanceof guard

Для проверки принадлежности к классу:

class Dog {
  bark { return "Woof!"; }
}

class Cat {
  meow { return "Meow!"; }
}

function makeSound(animal: Dog | Cat): string {
  if (animal instanceof Dog) {
    return animal.bark; // animal: Dog
  }
  return animal.meow; // animal: Cat
}

// Для встроенных типов
function formatDate(value: string | Date): string {
  if (value instanceof Date) {
    return value.toISOString(); // value: Date
  }
  return new Date(value).toISOString(); // value: string
}

// Для Error
function handleError(error: unknown): string {
  if (error instanceof Error) {
    return error.message; // error: Error
  }
  return String(error);
}

in operator guard

Проверка наличия свойства в объекте:

interface Bird {
  fly: void;
  layEggs: void;
}

interface Fish {
  swim: void;
  layEggs: void;
}

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

// Discriminated union с in
interface SuccessResponse {
  data: unknown;
  status: number;
}

interface ErrorResponse {
  error: string;
  code: number;
}

function handleResponse(res: SuccessResponse | ErrorResponse): void {
  if ("data" in res) {
    console.log(res.data); // SuccessResponse
  } else {
    console.log(res.error); // ErrorResponse
  }
}

Truthiness narrowing

// Проверка на null/undefined
function greet(name: string | null | undefined): string {
  if (name) {
    return `Hello, ${name}!`; // name: string
  }
  return "Hello, stranger!";
}

// Двойная отрицание для boolean
function process(value: string | null): void {
  if (!!value) {
    console.log(value.length); // value: string
  }
}

// Осторожно с falsy-значениями!
function handleNumber(value: number | null): number {
  if (value) {
    return value; // value: number, НО 0 тоже falsy!
  }
  return -1; // value: number | null (0 попадёт сюда!)
}

// Правильно для чисел:
function handleNumber(value: number | null): number {
  if (value !== null) {
    return value; // value: number (0 корректно обработан)
  }
  return -1;
}

Equality narrowing

function compare(a: string | number, b: string | boolean): void {
  if (a === b) {
    // a: string, b: string — единственный общий тип
    console.log(a.toUpperCase());
  }
}

// switch/case для discriminated unions
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "triangle"; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2; // shape: { kind: "circle"; radius: number }
    case "square":
      return shape.side ** 2;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

User-Defined Type Guards (is keyword)

Пользовательские функции, которые сообщают TypeScript о сужении типа:

// Синтаксис: параметр is Тип
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function isNumber(value: unknown): value is number {
  return typeof value === "number";
}

function process(value: unknown): string {
  if (isString(value)) {
    return value.toUpperCase(); // value: string
  }
  if (isNumber(value)) {
    return value.toFixed(2); // value: number
  }
  return "unknown";
}

Сложные type guards

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

// Проверка объекта из внешнего источника
function isUser(data: unknown): data is User {
  return (
    typeof data === "object" &&
    data !== null &&
    "id" in data &&
    "name" in data &&
    "email" in data &&
    typeof (data as User).id === "number" &&
    typeof (data as User).name === "string" &&
    typeof (data as User).email === "string"
  );
}

// Использование
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data: unknown = await response.json();

  if (isUser(data)) {
    return data; // data: User — типобезопасно
  }
  throw new Error("Invalid user data");
}

// Type guard для массивов
function isArrayOf<T>(
  arr: unknown,
  guard: (item: unknown) => item is T
): arr is T {
  return Array.isArray(arr) && arr.every(guard);
}

function isUserArray(data: unknown): data is User {
  return isArrayOf(data, isUser);
}

Type guard в filter

const mixed: (string | number | null) = [1, "hello", null, 42, "world", null];

// Без type guard — тип не сужается
const strings1 = mixed.filter((x) => typeof x === "string");
// (string | number | null) — не сужает!

// С type guard — правильное сужение
const strings2 = mixed.filter((x): x is string => typeof x === "string");
// string

const nonNull = mixed.filter((x): x is string | number => x !== null);
// (string | number)

Assertion Functions

Функции, которые гарантируют тип или выбрасывают исключение:

// asserts condition
function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

function process(value: string | null): string {
  assert(value !== null, "Value must not be null");
  // После assert: value: string
  return value.toUpperCase();
}

// asserts параметр is Тип
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new TypeError(`Expected string, got ${typeof value}`);
  }
}

function handle(input: unknown): string {
  assertIsString(input);
  // После assertion: input: string
  return input.toUpperCase();
}

// Assertion для объектов
function assertIsUser(data: unknown): asserts data is User {
  if (!isUser(data)) {
    throw new Error("Invalid user data");
  }
}

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

  1. Type guard врёт — TypeScript доверяет is без проверки
// ОПАСНО: type guard утверждает string, но не проверяет!
function isString(value: unknown): value is string {
  return true; // Всегда true — TypeScript НЕ проверяет!
}

const num = 42;
if (isString(num)) {
  num.toUpperCase(); // Компилируется, но упадёт в рантайме!
}
  1. typeof null === "object" — нужна дополнительная проверка !== null
  2. Falsy-значения0, "", false, NaN — falsy, но валидные
  3. Потеря сужения — присвоение в переменную может потерять сужение
function process(value: string | number) {
  const isStr = typeof value === "string";
  if (isStr) {
    value.toUpperCase(); // OK в TS 4.4+ (control flow через const)
  }
}
  1. instanceof не работает с интерфейсами — только с классами
interface User { name: string; }
// if (data instanceof User) // Ошибка! Interface не существует в рантайме
// Используйте in или пользовательский type guard

Практика

  1. Напишите type guard isNonNull<T>(value: T | null | undefined): value is T
  2. Создайте type guard для проверки API-ответа: isApiError(data: unknown): data is ApiError
  3. Используйте type guard с Array.filter для фильтрации null из массива
  4. Напишите assertion function assertDefined<T>(value: T | undefined): asserts value is T
  5. Реализуйте exhaustive check с never в switch для discriminated union

Связанные темы

Ресурсы