Пользовательские Type Guards

Пользовательский type guard — функция с сигнатурой (val: unknown) => val is T, которая возвращает boolean, и при возврате true TypeScript сужает тип val до T в вызывающем коде.

Зачем нужно

Встроенные type guards (typeof, instanceof) покрывают только примитивы и классы. Для сложных объектных типов, union-типов с runtime-структурой и валидации внешних данных необходимы пользовательские guard-функции.

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

  • Проверка структуры внешних данных (API, JSON.parse, localstorage)
  • Различение вариантов union-типа по полю или структуре
  • Проверка что объект реализует интерфейс
  • Обёртки над zod/yup для интеграции с narrowing
  • Тесты: type-safe проверка типов данных

Основной контент

Базовый синтаксис

// Синтаксис: (val: any) => val is T
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function processValue(value: string | number): void {
  if (isString(value)) {
    // value: string — TypeScript знает
    console.log(value.toUpperCase());
  } else {
    // value: number
    console.log(value.toFixed(2));
  }
}

Type guard для интерфейса

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

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

const raw: unknown = JSON.parse(localStorage.getItem("user") ?? "{}");
if (isUser(raw)) {
  console.log(raw.name); // TypeScript знает: raw is User
}

Type guard для discriminated union

interface SuccessResult<T> { ok: true; data: T }
interface FailureResult { ok: false; error: string }
type Result<T> = SuccessResult<T> | FailureResult;

function isSuccess<T>(result: Result<T>): result is SuccessResult<T> {
  return result.ok === true;
}

function isFailure<T>(result: Result<T>): result is FailureResult {
  return result.ok === false;
}

const result: Result<User> = await fetchUser("u-1");

if (isSuccess(result)) {
  console.log(result.data.name); // data: User
} else {
  console.error(result.error);   // error: string
}

Generic type guard

// Универсальный guard для массива
function isArrayOf<T>(
  value: unknown,
  itemGuard: (item: unknown) => item is T
): value is T {
  return Array.isArray(value) && value.every(itemGuard);
}

const raw: unknown = JSON.parse(data);
if (isArrayOf(raw, isUser)) {
  // raw: User
  raw.forEach(user => console.log(user.name));
}

Exhaustive type guard

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "triangle"; base: number; height: number };

function isCircle(s: Shape): s is Extract<Shape, { kind: "circle" }> {
  return s.kind === "circle";
}

// Exhaustive helper
function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
}

function getArea(shape: Shape): number {
  if (isCircle(shape)) return Math.PI * shape.radius ** 2;
  if (shape.kind === "square") return shape.side ** 2;
  if (shape.kind === "triangle") return (shape.base * shape.height) / 2;
  return assertNever(shape);
}

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

  • Возвращать true без реальной проверкиfunction isUser(x: unknown): x is User { return true; } обманет TypeScript; runtime сломается.
  • Не проверять nulltypeof value === "object" возвращает true для null; всегда добавляйте value !== null.
  • Использовать as User внутри guard без проверок — это просто cast, не верификация; guard должен действительно проверять структуру.
  • Не обновлять guard при изменении интерфейса — если добавить поле в интерфейс и не добавить проверку в guard — TypeScript не предупредит.

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

Ресурсы