1. Что такое Type Guards и зачем они нужны
Представьте, что вы работаете на складе. К вам приходит коробка, и на ней написано «инструменты». Но внутри может быть что угодно — молоток, отвёртка или даже что-то совсем неожиданное. Чтобы правильно использовать содержимое, вам нужно открыть коробку и посмотреть, что именно внутри.
Type Guards в TypeScript работают точно так же. Они позволяют TypeScript вruntime (во время выполнения программы) проверить, какой именно тип данных перед нами, и на основании этой проверки сузить (narrow) тип до более конкретного.
Почему это важно? TypeScript работает со статической типизацией — он проверяет типы во время компиляции. Но иногда TypeScript не может определить точный тип переменной только по статическому анализу. Например, когда вы получаете данные от пользователя, из API или из внешнего мира — TypeScript просто не знает наверняка, что там находится. Именно здесь на помощь приходят Type Guards.
Аналогия: Type Guard — это как проверка напроверка (контрольная точка) в аэропорту. Пока вы не пройдёте проверку, система не может точно знать, кто вы — пассажир, пилот или сотрудник наземного обслуживания. Проверка определяет вашу роль и даёт вам соответствующие права и возможности.
Рассмотрим простую проблему. Допустим, у нас есть функция, которая принимает строку или число:
function processValue(value: string | number) {
// TypeScript НЕ ЗНАЕТ, строка это или число
// Поэтому он запретит нам вызывать методы строки
console.log(value.toUpperCase()); // ❌ Ошибка!
// Property 'toUpperCase' does not exist on type 'string | number'
}
TypeScript справедливоуказывает: «Подожди, я не могу дать тебе доступ к методу toUpperCase, потому что это число, а у чисел такого метода нет!» Это правильная и безопасная проверка. Но что делать, если мы точно знаем, что значение — строка? Как сообщить об этом TypeScript?
Вот тут и приходят на помощь Type Guards. Они работают как «условие проверки», после которого TypeScript «понимает», что тип сузился:
function processValue(value: string | number) {
if (typeof value === "string") {
// Здесь TypeScript УЖЕ ЗНАЕТ, что value — строка!
console.log(value.toUpperCase()); // ✅ Всё работает!
} else {
// А здесь TypeScript знает, что value — число
console.log(value.toFixed(2)); // ✅ И это тоже работает!
}
}
Посмотрите, как изменилось поведение: после проверки typeof value === "string" TypeScript «сузил» тип переменной value внутри блока if только до string. Это и есть type narrowing (сужение типа), а проверка, которая это делает — Type Guard.
TypeScript предоставляет несколько встроенных механизмов для создания Type Guards. Давайте рассмотрим каждый из них подробно.
2. typeof Type Guard — проверка примитивных типов
Оператор typeof — это самый простой и распространённый Type Guard. Он проверяетво время выполнения тип значения и возвращает строку, описывающую этот тип. В JavaScript typeof может вернуть одно из семи значений:
"string"
— строка
"number"
— число
"boolean"
— логическое
"bigint"
— большое число
"symbol"
— символ
"undefined"
— не определено
"function"
— функция
"object"
— объект (включая массивы и null)
Важно помнить: typeof null возвращает "object" — это историческая особенность JavaScript, не баг. Также typeof не различает массивы и обычные объекты — оба возвращают "object".
Рассмотрим практический пример. Допустим, мы создаём калькулятор, который может работать с разными типами данных:
function calculate(value: string | number | boolean) {
if (typeof value === "string") {
// TypeScript теперь знает: value — это string
console.log("Длина строки:", value.length);
console.log("В верхнем регистре:", value.toUpperCase());
return value.length;
}
if (typeof value === "number") {
// TypeScript теперь знает: value — это number
console.log("Число:", value);
console.log("Квадрат:", value ** 2);
return value ** 2;
}
// Остался только boolean
console.log("Логическое значение:", value);
return value ? 1 : 0;
}
calculate("Hello"); // Длина строки: 5
calculate(7); // Квадрат: 49
calculate(true); // Логическое значение: true
Обратите внимание на ключевой момент: после каждого if (typeof value === "...") внутри соответствующего блока TypeScript автоматически «сужает» тип переменной. Это происходит автоматически — вам не нужно писать никаких аннотаций.
Важно: typeof проверяет примитивные типы. Для проверки объектов и классов используйте instanceof, in или другие методы, которые мы рассмотрим далее. typeof для объектов вернёт просто "object", что почти бесполезно для различения разных типов объектов.
Ещё один важный аспект — typeof можно использовать в цепочках проверок. TypeScript понимает логику «если не строка и не число, значит...»:
function describe(input: string | number | boolean | undefined) {
if (typeof input === "string") {
return `Строка длиной ${input.length}`;
}
if (typeof input === "number") {
return `Число: ${input}`;
}
if (typeof input === "boolean") {
return input ? "Да" : "Нет";
}
// Здесь TypeScript знает: input — undefined
return "Значение не определено";
}
Совет: Всегда проверяйте typeof через строгое сравнение ===, а не через ==. Строгое сравнение безопаснее и предсказуемее. TypeScript даже может помочь вам: он разрешает сравнивать typeof только с допустимыми строковыми литералами.
3. instanceof Type Guard — проверка экземпляров классов
Если typeof — это проверка примитивов, то instanceof — это его «старший брат» для проверки объектов. Оператор instanceof проверяет, был ли объект создан определённым классом.
Аналогия: Представьте, что у вас есть разные типы транспорта — автомобиль, велосипед и автобус. Когда вы стоите рядом с транспортом и закрыты глаза, вы не знаете, что это. Но когда вы открываете глаза и видите «колёса, кузов, номерной знак» — вы понимаете, что это автомобиль. instanceof — это как «открыть глаза» и проверить, является ли объект экземпляром конкретного класса.
class Cat {
meow(): string {
return "Мяу!";
}
purr(): string {
return "Мррр...";
}
}
class Dog {
bark(): string {
return "Гав!";
}
fetch(): string {
return "Принёс палочку!";
}
}
function makeSound(animal: Cat | Dog) {
if (animal instanceof Cat) {
// TypeScript теперь знает: animal — это Cat
console.log(animal.meow()); // ✅
console.log(animal.purr()); // ✅
} else {
// TypeScript теперь знает: animal — это Dog
console.log(animal.bark()); // ✅
console.log(animal.fetch()); // ✅
}
}
instanceof работает проверяя цепочку прототипов объекта. Если объект был создан через new Cat(), то animal instanceof Cat вернёт true, потому что объект наследует от Cat.prototype.
Вот более реалистичный пример — система обработки ошибок:
class ValidationError extends Error {
constructor(message: string, public field: string) {
super(message);
this.name = "ValidationError";
}
}
class NetworkError extends Error {
constructor(message: string, public statusCode: number) {
super(message);
this.name = "NetworkError";
}
}
class DatabaseError extends Error {
constructor(message: string, public query: string) {
super(message);
this.name = "DatabaseError";
}
}
function handleError(error: Error) {
if (error instanceof ValidationError) {
console.log(`Ошибка в поле "${error.field}": ${error.message}`);
// TypeScript знает: error — ValidationError
// У нас есть доступ к error.field
} else if (error instanceof NetworkError) {
console.log(`Сетевая ошибка ${error.statusCode}: ${error.message}`);
// TypeScript знает: error — NetworkError
// У нас есть доступ к error.statusCode
} else if (error instanceof DatabaseError) {
console.log(`Ошибка в запросе "${error.query}": ${error.message}`);
// TypeScript знает: error — DatabaseError
// У нас есть доступ к error.query
} else {
console.log(`Неизвестная ошибка: ${error.message}`);
}
}
Важно: instanceof НЕ работает с примитивными типами (string, number, boolean). Вы не можете написать "hello" instanceof String — это всегда вернёт false. Для примитивов используйте typeof.
instanceof также работает с встроенными классами JavaScript:
function processInput(input: string | Date | RegExp | Array<number>) {
if (input instanceof Date) {
console.log("Дата:", input.toISOString());
} else if (input instanceof RegExp) {
console.log("Регулярное выражение:", input.source);
} else if (input instanceof Array) {
console.log("Массив длиной:", input.length);
} else {
// Осталась строка
console.log("Строка:", input);
}
}
4. in Type Guard — проверка наличия свойства
Оператор in проверяет, существует ли определённое свойство в объекте. Это особенно полезно, когда у вас есть объекты с разной структурой (разными свойствами), и вы хотите определить, с каким из них имеете дело.
Аналогия: Представьте, что вы получили посылку. Вы не знаете, что внутри, но можете проверить: «Есть ли в этой посылке ручка?» Если да — это чемодан. «Есть ли крышка?» Если да — это коробка. Оператор in работает точно так — проверяет наличие «ручек» (свойств) в объекте.
interface Fish {
swim: () => void;
fins: number;
}
interface Bird {
fly: () => void;
wings: number;
}
function move(animal: Fish | Bird) {
if ("swim" in animal) {
// TypeScript теперь знает: animal — это Fish
animal.swim(); // ✅
console.log("Плавников:", animal.fins); // ✅
} else {
// TypeScript теперь знает: animal — это Bird
animal.fly(); // ✅
console.log("Крыльев:", animal.wings); // ✅
}
}
Оператор in проверяет наличие свойства в объекте, а не его значение. Это делает его отличным выбором для проверки объектов с разной структурой (разными наборами свойств).
Более реалистичный пример — обработка данных из формы:
interface UserProfile {
name: string;
email: string;
avatar: string;
}
interface Product {
title: string;
price: number;
image: string;
}
function renderCard(data: UserProfile | Product) {
if ("email" in data) {
// Это UserProfile — у него есть email
console.log(`Пользователь: ${data.name}`);
console.log(`Почта: ${data.email}`);
console.log(`Аватар: ${data.avatar}`);
} else {
// Это Product — у него есть price
console.log(`Товар: ${data.title}`);
console.log(`Цена: ${data.price} руб.`);
console.log(`Картинка: ${data.image}`);
}
}
Осторожно! Оператор in проверяет свойство в самом объекте и его прототипной цепочке. Если свойство существует, но его значение undefined, проверка всё равно вернёт true. Это может привести к неожиданным результатам.
5. Проверки равенства (=== и !==) как Type Guards
Самый простой Type Guard — это обычное сравнение с конкретным значением. Когда вы сравниваете переменную с конкретным литералом (строкой, числом, null, undefined), TypeScript автоматически сужает тип.
Аналогия: Это как проверка «Это яблоко? Нет? Тогда это что-то другое». Простая проверка на конкретное значение мгновенно проясняет ситуацию.
function processId(id: string | number | null) {
if (id === null) {
console.log("ID отсутствует");
return;
}
// Теперь id: string | number (null исключён)
if (id === "admin") {
// Теперь id: "admin" (строковый литерал!)
console.log("Привет, администратор!");
console.log(id.toUpperCase()); // ✅
} else if (id === 0) {
// Теперь id: 0 (числовой литерал!)
console.log("Нулевой ID");
} else {
// Теперь id: string | number (кроме "admin" и 0)
console.log("ID:", id);
}
}
Обратите внимание: после проверки id === "admin" TypeScript знает, что id — это именно строка "admin", а не просто string. Это называется литеральным типом (literal type) и является мощным инструментом.
Проверка на undefined и null особенно полезна при работе с опциональными значениями:
interface Config {
debug?: boolean;
verbose?: boolean;
}
function setup(config: Config) {
// config.debug: boolean | undefined
// config.verbose: boolean | undefined
if (config.debug === true) {
// config.debug: true
console.log("Отладка включена");
}
if (config.debug !== undefined) {
// config.debug: boolean (undefined исключён)
console.log("Debug:", config.debug);
}
// Самый частый паттерн — проверка на truthiness
if (config.debug) {
// config.debug: true (false и undefined исключены)
console.log("Отладка активна");
}
}
Когда использовать: Проверки равенства идеально подходят для работы с литеральными типами, перечислениями (enums) и nullable типами. Это самый быстрый и понятный способ сузить тип.
6. Truthiness проверки как Type Guards
В JavaScript есть понятие «truthiness» (истинность) — любое значение можно использовать в булевом контексте (в if, &&, ||, !). Значения, которые JavaScript считает «ложными» (falsy): false, 0, "" (пустая строка), null, undefined, NaN. Все остальные значения — «истинные» (truthy).
Truthiness проверки — это самый распространённый способ работы с опциональными значениями:
function greetUser(name: string | null | undefined) {
if (name) {
// TypeScript знает: name — это string (не null, не undefined)
console.log(`Привет, ${name}!`);
console.log(`Длина имени: ${name.length}`);
} else {
// TypeScript знает: name — это null | undefined
console.log("Привет, анонимный пользователь!");
}
}
greetUser("Алиса"); // Привет, Алиса!
greetUser(null); // Привет, анонимный пользователь!
greetUser(undefined); // Привет, анонимный пользователь!
Truthiness проверки особенно полезны при работе с API, которые могут вернуть пустые значения:
interface ApiResponse {
data?: {
items: string[];
total: number;
};
error?: string;
}
function processResponse(response: ApiResponse) {
// data может быть undefined
if (!response.data) {
console.log("Нет данных");
return;
}
// Теперь response.data существует!
console.log(`Всего: ${response.data.total}`);
console.log(`Элементы: ${response.data.items.join(", ")}`);
// Дополнительная проверка на пустой массив
if (response.data.items.length > 0) {
response.data.items.forEach(item => {
console.log("-", item);
});
}
}
Ещё один полезный паттерн — использование truthiness для проверки на null или undefined при работе с optional chaining:
interface Company {
ceo?: {
name: string;
email?: string;
};
}
function getCEOEmail(company: Company): string {
// Без проверки — ошибка!
// return company.ceo.email; // ❌ Object is possibly 'undefined'
// С optional chaining — безопасно, но тип string | undefined
const email = company.ceo?.email;
// С truthiness проверкой — тип string
if (email) {
return email; // ✅ TypeScript знает: email — это string
}
return "Email не указан";
}
Осторожно с числовыми значениями! Truthiness проверка if (count) вернёт false для числа 0, даже если 0 — это валидное значение. В таких случаях лучше использовать явную проверку if (count !== undefined && count !== null) или if (count != null).
7. Пользовательские Type Guards — Type Predicate
Встроенные Type Guards (typeof, instanceof, in) отлично справляются с базовыми проверками. Но иногда вам нужно проверить что-то более сложное — например, что объект соответствует определённой структуре, что массив содержит только определённые типы, или что строка соответствует определённому формату.
Для этого TypeScript позволяет создавать пользовательские Type Guards с помощью type predicate — специального возвращаемого типа функции.
Анализ: Представьте, что вы — эксперт по вину. Вы не просто говорите «это красное» или «это белое». Вы говорите: «Я проверю и скажу вам, что это за вино». Ваша экспертиза — это пользовательский Type Guard. Вы принимаете «сырой» объект и возвращаете заключение о его типе.
Синтаксис type predicate: функция возвращает value is SomeType вместо обычного boolean:
// Обычная функция — возвращает boolean
function isString(value: unknown): boolean {
return typeof value === "string";
}
// Type Predicate — возвращает value is string
function isString(value: unknown): value is string {
return typeof value === "string";
}
Разница в том, что value is string говорит TypeScript: «Если эта функция вернёт true, то value — это string». TypeScript не проверяет, правда ли это — он доверяет вам на слово.
function processValue(value: string | number) {
if (isString(value)) {
// TypeScript верит: value — string
console.log(value.toUpperCase()); // ✅
}
}
// ⚠️ ВАЖНО: TypeScript доверяет вам!
function isNumber(value: unknown): value is number {
return typeof value === "string"; // Врём — это строка!
}
function broken(value: unknown) {
if (isNumber(value)) {
// TypeScript ДУМАЕТ, что value — number
console.log(value.toFixed(2)); // ✅ Компилируется...
// Но runtime: value.toFixed is not a function!
// 💥 ОШИБКА ВЫПОЛНЕНИЯ!
}
}
Критически важно: TypeScript не проверяет, что ваш type predicate действительно возвращает корректное значение. Вы обещаете TypeScript, что проверка верна. Если ваша функция возвращает false неправильно — TypeScript будет считать, что тип сузился, хотя на самом деле это не так. Это может привести к ошибкам во время выполнения программы!
8. Type Predicate Functions — практические примеры
Давайте рассмотрим реальные сценарии, где пользовательские Type Guards незаменимы.
Пример 1: Фильтрация массива по типу
interface SuccessResult {
status: "success";
data: unknown;
}
interface ErrorResult {
status: "error";
message: string;
}
type Result = SuccessResult | ErrorResult;
function isSuccess(result: Result): result is SuccessResult {
return result.status === "success";
}
function isError(result: Result): result is ErrorResult {
return result.status === "error";
}
const results: Result[] = [
{ status: "success", data: { name: "Алиса" } },
{ status: "error", message: "Сервер недоступен" },
{ status: "success", data: { name: "Боб" } },
];
// Фильтруем только успешные результаты
const successfulResults = results.filter(isSuccess);
// Тип: SuccessResult[]
const failedResults = results.filter(isError);
// Тип: ErrorResult[]
successfulResults.forEach(result => {
console.log("Данные:", result.data); // ✅ data доступно
});
failedResults.forEach(result => {
console.log("Ошибка:", result.message); // ✅ message доступно
});
Пример 2: Проверка строки на формат
function isEmail(value: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
function isPhoneNumber(value: string): boolean {
return /^\+?[1-9]\d{1,14}$/.test(value);
}
function processContact(input: string) {
if (isEmail(input)) {
console.log(`Отправляем письмо на ${input}`);
// Но TypeScript НЕ ЗНАЕТ, что input — email!
// Потому что isEmail возвращает boolean, а не type predicate
}
}
Заметьте: хотя isEmail проверяет формат, TypeScript не может использовать это для сужения типа, потому что функция возвращает обычный boolean. Это нормально для многих сценариев, но если вам нужно, чтобы TypeScript знал больше — используйте type predicate.
Пример 3: Проверка объекта на соответствие интерфейсу
interface User {
id: number;
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 === "number" &&
typeof (value as User).name === "string" &&
typeof (value as User).email === "string"
);
}
function processData(data: unknown) {
if (isUser(data)) {
// TypeScript теперь знает: data — User
console.log(`Пользователь: ${data.name}`); // ✅
console.log(`Email: ${data.email}`); // ✅
}
}
// Использование с API
async function fetchUser() {
const response = await fetch("/api/user");
const data = await response.json();
if (isUser(data)) {
// Безопасно работаем с данными
renderUser(data);
} else {
console.error("Неверный формат данных от API");
}
}
Совет: При создании пользовательских Type Guards для сложных интерфейсов рассмотрите использование библиотек вроде zod, io-ts или superstruct. Они автоматически генерируют type guards из схем и снижают риск ошибок.
9. Assertion Functions — функции утверждения
Assertion functions — это особый вид Type Guards, которые выбрасывают ошибку вместо возврата false, если проверка не прошла. Они используют аннотацию asserts value is Type.
Аналогия: Представьте охранника на входе. Если у вас нет пропуска, он не просто скажет «нет» и отпустит — он остановит вас и не пустит дальше. Assertion functions работают так же: если проверка не пройдена — функция выбрасывает ошибку, и выполнение кода прекращается.
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Ожидалась строка, получено: ${typeof value}`);
}
}
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== "number") {
throw new Error(`Ожидалось число, получено: ${typeof value}`);
}
}
function processInput(input: unknown) {
// Если input не строка — выбросится ошибка
assertIsString(input);
// TypeScript теперь ЗНАЕТ: input — это string
console.log(input.toUpperCase()); // ✅ Безопасно!
// Если input не число — выбросится ошибка
assertIsNumber(input);
console.log(input.toFixed(2)); // ✅ Безопасно!
}
Assertion functions особенно полезны для валидации данных на границах системы:
interface Config {
port: number;
host: string;
debug: boolean;
}
function assertConfig(value: unknown): asserts value is Config {
if (typeof value !== "object" || value === null) {
throw new Error("Config must be an object");
}
const config = value as Record<string, unknown>;
if (typeof config.port !== "number") {
throw new Error("Config.port must be a number");
}
if (typeof config.host !== "string") {
throw new Error("Config.host must be a string");
}
if (typeof config.debug !== "boolean") {
throw new Error("Config.debug must be a boolean");
}
}
function loadConfig(rawConfig: unknown): Config {
// Если конфиг невалиден — выбросится ошибка
assertConfig(rawConfig);
// TypeScript теперь знает: rawConfig — Config
return rawConfig; // ✅
}
// Использование
try {
const config = loadConfig({ port: 3000, host: "localhost", debug: true });
console.log(`Сервер запущен на ${config.host}:${config.port}`);
} catch (error) {
console.error("Ошибка конфигурации:", error);
}
Когда использовать assertion functions: Когда вы хотите гарантировать, что тип правильный, и готовы остановить выполнение программы, если это не так. Assertion functions идеально подходят для проверки входных данных в начале функции.
10. Discriminated Unions — дисиминированные объединения
Discriminated unions (дисиминированные объединения) — это один из самых мощных паттернов в TypeScript для работы с разными типами данных. Он основан на общей «метке» (discriminant), которая определяет, какой именно тип данных перед нами.
Аналогия: Представьте, что у вас есть ящики с цветными наклейками: красная — «хрупкое», синяя — «жидкость», зелёная — «пища». Чтобы понять, как обращаться с содержимым, вам не нужно открывать каждый ящик — достаточно посмотреть на цвет наклейки. Discriminant — это как цветная наклейка на ящике.
interface Circle {
kind: "circle";
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
// TypeScript знает: shape — Circle
return Math.PI * shape.radius ** 2;
case "rectangle":
// TypeScript знает: shape — Rectangle
return shape.width * shape.height;
case "triangle":
// TypeScript знает: shape — Triangle
return (shape.base * shape.height) / 2;
}
}
const circle: Circle = { kind: "circle", radius: 5 };
const rect: Rectangle = { kind: "rectangle", width: 4, height: 6 };
console.log(calculateArea(circle)); // 78.54
console.log(calculateArea(rect)); // 24
Discriminant — это поле с литеральным типом ("circle", "rectangle", "triangle"), которое присутствует во всех вариантах объединения. TypeScript использует его для определения конкретного типа.
Вот более реалистичный пример — система событий:
interface UserLoginEvent {
type: "LOGIN";
userId: string;
timestamp: number;
}
interface UserLogoutEvent {
type: "LOGOUT";
userId: string;
timestamp: number;
}
interface UserPurchaseEvent {
type: "PURCHASE";
userId: string;
productId: string;
amount: number;
timestamp: number;
}
type UserEvent = UserLoginEvent | UserLogoutEvent | UserPurchaseEvent;
function handleEvent(event: UserEvent) {
switch (event.type) {
case "LOGIN":
console.log(`Пользователь ${event.userId} вошёл в систему`);
// TypeScript знает: event — UserLoginEvent
break;
case "LOGOUT":
console.log(`Пользователь ${event.userId} вышел из системы`);
// TypeScript знает: event — UserLogoutEvent
break;
case "PURCHASE":
console.log(`${event.userId} купил товар ${event.productId}`);
console.log(`Сумма: ${event.amount} руб.`);
// TypeScript знает: event — UserPurchaseEvent
break;
}
}
Преимущество: Discriminated unions позволяют TypeScript автоматически определять тип во всех ветвях switch или if. Если вы добавите новый вариант в объединение, TypeScript покажет ошибку во всех местах, где вы не обработали новый тип — это помогает не забыть о новых вариантах!
11. Type Narrowing Chains — комбинирование нескольких проверок
В реальных приложениях часто нужно комбинировать несколько Type Guards. TypeScript поддерживает цепочки сужения (narrowing chains) — после каждой проверки тип сузится, и следующая проверка будет работать уже с более узким типом.
type Primitive = string | number | boolean | null | undefined | symbol | bigint;
function describeValue(value: Primitive): string {
// Начальный тип: Primitive
if (value === null) {
// Тип: null
return "Это null";
}
// Тип: string | number | boolean | undefined | symbol | bigint
if (value === undefined) {
// Тип: undefined
return "Это undefined";
}
// Тип: string | number | boolean | symbol | bigint
if (typeof value === "boolean") {
// Тип: boolean
return value ? "Это true" : "Это false";
}
// Тип: string | number | symbol | bigint
if (typeof value === "string") {
// Тип: string
return `Строка "${value}" длиной ${value.length}`;
}
// Тип: number | symbol | bigint
if (typeof value === "number") {
// Тип: number
return `Число ${value}`;
}
// Тип: symbol | bigint
if (typeof value === "bigint") {
// Тип: bigint
return `Большое число ${value}`;
}
// Тип: symbol
return `Символ: ${value.toString()}`;
}
TypeScript также понимает негативные проверки. Если вы написали if (typeof value === "string") и не сделали return, TypeScript будет знать, что во внешнем коде value — это не строка:
function process(value: string | number | boolean) {
if (typeof value === "string") {
console.log("Это строка:", value.toUpperCase());
return;
}
// Здесь TypeScript знает: value — number | boolean
// (string исключён, потому что мы сделали return выше)
if (typeof value === "number") {
console.log("Это число:", value.toFixed(2));
return;
}
// Здесь TypeScript знает: value — boolean
// (string и number исключены)
console.log("Это булево значение:", value);
}
TypeScript также поддерживает else для сужения типа:
function check(value: string | number) {
if (typeof value === "string") {
// value: string
console.log("Длина:", value.length);
} else {
// value: number (в ветке else, если в if НЕТ return)
console.log("Квадрат:", value ** 2);
}
// После if-else с return: тип снова string | number
// TypeScript «забывает» сужение после выхода из блока
}
Совет: Если в вашей функции много вариантов, рассмотрите использование switch с discriminated unions вместо длинных цепочек if-else. Это делает код чище и помогает TypeScript лучше отслеживать сужение типа.
12. Паттерны Type Guards в реальных приложениях
Давайте рассмотрим, как Type Guards используются в реальных проектах.
Паттерн 1: Safe API Response Handler
interface ApiResponse<T> {
data?: T;
error?: string;
status: number;
}
function isSuccessResponse<T>(
response: ApiResponse<T>
): response is ApiResponse<T> & { data: T } {
return response.status >= 200 && response.status < 300;
}
async function fetchUser(id: string) {
const response = await fetch(`/api/users/${id}`);
const result: ApiResponse<User> = await response.json();
if (isSuccessResponse(result)) {
// TypeScript знает: result.data — это User (не undefined)
console.log(`Пользователь: ${result.data.name}`);
} else {
console.error(`Ошибка: ${result.error}`);
}
}
Паттерн 2: Type Guard для проверки Optional Properties
function hasProperty<T, K extends string>(
obj: T,
key: K
): obj is T & Record<K, unknown> {
return typeof obj === "object" && obj !== null && key in obj;
}
function processObject(obj: unknown) {
if (typeof obj !== "object" || obj === null) return;
if (hasProperty(obj, "name") && typeof obj.name === "string") {
console.log(`Имя: ${obj.name}`);
}
if (hasProperty(obj, "age") && typeof obj.age === "number") {
console.log(`Возраст: ${obj.age}`);
}
}
Паттерн 3: Exhaustive Check (проверка полноты)
type Direction = "up" | "down" | "left" | "right";
function move(direction: Direction): string {
switch (direction) {
case "up":
return "Вверх";
case "down":
return "Вниз";
case "left":
return "Влево";
case "right":
return "Вправо";
default: {
// Если добавить новый Direction и забыть обработать —
// TypeScript покажет ошибку здесь!
const _exhaustiveCheck: never = direction;
return _exhaustiveCheck;
}
}
}
Паттерн Exhaustive Check: Используйте never в ветке default switch-оператора для discriminated unions. Если вы добавите новый вариант в объединение, TypeScript покажет ошибку в ветке default, напомнив, что вы забыли его обработать. Это один из самых мощных приёмов для обеспечения полноты обработки.
13. Типичные ошибки и подводные камни
Давайте рассмотрим самые частые ошибки, которые делают новички при работе с Type Guards.
Ошибка 1: Забыли, что typeof не различает объекты
function checkType(value: unknown) {
if (typeof value === "object") {
// Это может быть объект, массив, null или что угодно!
// TypeScript знает только: object | null
console.log(value.length); // ❌ Ошибка!
// 'value' is of type 'object | null'
// Добавляем проверку на null
if (value !== null) {
// Теперь value: object
console.log(value); // ✅ Но это всё ещё object, не array!
}
}
}
Ошибка 2: Забыли, что instanceof не работает с примитивами
function check(value: string | number) {
// ❌ НЕПРАВИЛЬНО!
if (value instanceof String) {
// Это всегда false для примитивов!
console.log(value.toUpperCase());
}
// ✅ ПРАВИЛЬНО!
if (typeof value === "string") {
console.log(value.toUpperCase());
}
}
Ошибка 3: Путаница между null и undefined
function process(value: string | null | undefined) {
// ❌ Эта проверка исключает ТОЛЬКО null
if (value !== null) {
// value: string | undefined
// Можем получить ошибку, если value === undefined
}
// ✅ Эта проверка исключает И null, И undefined
if (value != null) {
// value: string
// Безопасно!
console.log(value.toUpperCase());
}
}
Ошибка 4: Type Guard не охватывает все варианты
type Status = "active" | "inactive" | "pending";
function getStatusText(status: Status): string {
if (status === "active") {
return "Активен";
}
if (status === "inactive") {
return "Неактивен";
}
// Забыли обработать "pending"!
// TypeScript покажет ошибку: не все варианты обработаны
// и функция вернёт undefined
}
Ошибка 5: Использование truthiness проверки для числовых значений
function processCount(count: number | undefined) {
// ❌ НЕПРАВИЛЬНО!
// Если count === 0, это валидное значение, но проверка вернёт false
if (count) {
console.log(`Количество: ${count}`);
} else {
console.log("Количество не указано");
// Пользователь может получить 0, но мы скажем "не указано"
}
// ✅ ПРАВИЛЬНО!
if (count !== undefined) {
console.log(`Количество: ${count}`);
} else {
console.log("Количество не указано");
}
}
Ошибка 6: Изменение переменной внутри Type Guard
function process(value: string | number) {
if (typeof value === "string") {
console.log(value.toUpperCase()); // ✅
// ❌ НЕПРАВИЛЬНО!
// Если изменить тип переменной после проверки,
// TypeScript может «забыть» о сужении
value = 42; // TypeScript может показать предупреждение
console.log(value); // Теперь number, но код выше думает, что string
}
}
Золотое правило: Не изменяйте переменные после Type Guard проверки. Если вам нужно работать с разными типами, создавайте новые переменные, а не изменяйте исходную. TypeScript отслеживает сужение типа в рамках одной переменной, и изменение может сломать эту логику.
14. Итоги урока
Type Guards — это один из самых важных инструментов для работы с типами в TypeScript. Они позволяют TypeScript пониматьво время выполнения типы данных и безопасно работать с ними.
Краткая шпаргалка:
value is Type
asserts value is Type
Что мы выучили:
- Type Guards позволяют TypeScript «понять»во время выполнения тип данных
- После проверки TypeScript автоматически сужает тип в соответствующем блоке кода
- Встроенные Type Guards (
typeof,instanceof,in) подходят для базовых проверок - Пользовательские Type Guards позволяют создавать сложные проверки
- Assertion functions выбрасывают ошибку при невалидных данных
- Discriminated unions — мощный паттерн для работы с несколькими типами
- TypeScript не проверяет корректность пользовательских Type Guards — доверяйте, но проверяйте!
Практический совет: Начинайте с простых Type Guards (typeof, ===) и переходите к более сложным (instanceof, in, пользовательские Type Guards) по мере необходимости. В 90% случаев базовых Type Guards достаточно для безопасного кода.
Тест
8 вопросов