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
Предпосылки
- Union и Intersection — union типы
- any unknown never void — unknown требует type guards
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");
}
}
Частые ошибки
- 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(); // Компилируется, но упадёт в рантайме!
}
- typeof null === "object" — нужна дополнительная проверка
!== null - Falsy-значения —
0,"",false,NaN— falsy, но валидные - Потеря сужения — присвоение в переменную может потерять сужение
function process(value: string | number) {
const isStr = typeof value === "string";
if (isStr) {
value.toUpperCase(); // OK в TS 4.4+ (control flow через const)
}
}
- instanceof не работает с интерфейсами — только с классами
interface User { name: string; }
// if (data instanceof User) // Ошибка! Interface не существует в рантайме
// Используйте in или пользовательский type guard
Практика
- Напишите type guard
isNonNull<T>(value: T | null | undefined): value is T - Создайте type guard для проверки API-ответа:
isApiError(data: unknown): data is ApiError - Используйте type guard с
Array.filterдля фильтрацииnullиз массива - Напишите assertion function
assertDefined<T>(value: T | undefined): asserts value is T - Реализуйте exhaustive check с
neverв switch для discriminated union
Связанные темы
- Union и Intersection — union типы для сужения
- any unknown never void — unknown требует type guards
- Conditional types — conditional types на уровне типов
- typeof и keyof — typeof в type context vs value context