Пользовательские Type Guards
Пользовательский type guard — функция с сигнатурой
(val: unknown) => val is T, которая возвращаетboolean, и при возвратеtrueTypeScript сужает тип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 сломается. - Не проверять
null—typeof value === "object"возвращаетtrueдляnull; всегда добавляйтеvalue !== null. - Использовать
as Userвнутри guard без проверок — это просто cast, не верификация; guard должен действительно проверять структуру. - Не обновлять guard при изменении интерфейса — если добавить поле в интерфейс и не добавить проверку в guard — TypeScript не предупредит.
Связанные темы
- Type Guards -- typeof и instanceof
- Assertion Functions
- Discriminated Unions
- in оператор для сужения
- any, unknown -- различия
- _MOC TypeScript