- Главная
- Typescript
- Type Narrowing, Guards и Aliases — контроль над типами в Typescript
Type Narrowing, Guards и Aliases — контроль над типами в Typescript
В прошлом уроке мы научились создавать Union Types, которые позволяют переменным принимать значения разных типов. Это даёт нам гибкость, но создаёт новую проблему: как работать с переменной, которая может быть строкой, числом или объектом? Ведь у каждого типа свои методы и свойства.
Сегодня мы разберём три мощных инструмента, которые решают эту задачу:
- Type Narrowing — процесс сужения широкого типа до более конкретного.
- Type Guards — механизмы, которые помогают TypeScript понять, с каким именно типом мы работаем.
- Type Aliases — способ дать имена сложным типам для переиспользования.
Проблема: Union Types без сужения
Напомню, с чем мы столкнулись в прошлом уроке:
function process(value: string | number): void {
// ❌ Ошибка: 'toUpperCase' не существует на типе 'number'
// console.log(value.toUpperCase());
// ❌ Ошибка: 'toFixed' не существует на типе 'string'
// console.log(value.toFixed(2));
// ✅ toString есть у обоих типов
console.log(value.toString());
}
Без сужения типа мы можем использовать только те методы и свойства, которые есть у всех типов в объединении. Чтобы использовать специфичные методы, нам нужно сузить тип.
Type Guards: проверяем типы в рантайме
Type Guard — это выражение, которое выполняет проверку во время выполнения и сообщает TypeScript, с каким типом мы имеем дело. Самый простой и надёжный способ — использовать оператор typeof.
typeof — для примитивов
function process(value: string | number): string | number {
if (typeof value === "string") {
// Внутри этого блока TypeScript знает, что value — это string
return value.toUpperCase();
} else {
// Здесь value точно number (потому что других вариантов нет)
return value * 2;
}
}
console.log(process("hello")); // "HELLO"
console.log(process(10)); // 20
typeof работает с примитивными типами: string, number, boolean, symbol, bigint, undefined, function, object.
Проверка на null и undefined
Одна из самых частых проблем в JavaScript — это работа с потенциально отсутствующими значениями. Type Guards помогают и здесь:
function printLength(text: string | null | undefined): void {
if (text === null || text === undefined) {
console.log("Текст отсутствует");
} else {
// TypeScript знает: text — это точно string (не null и не undefined)
console.log(`Длина текста: ${text.length}`);
}
}
// Более краткая версия с проверкой на falsy
function printLengthShort(text: string | null | undefined): void {
if (!text) {
console.log("Текст отсутствует");
} else {
console.log(`Длина текста: ${text.length}`);
}
}
instanceof — для классов
Для проверки экземпляров классов используется оператор instanceof:
class Dog {
bark() { console.log("Woof! Woof!"); }
}
class Cat {
meow() { console.log("Meow! Meow!"); }
}
function makeSound(animal: Dog | Cat): void {
if (animal instanceof Dog) {
animal.bark(); // TypeScript знает: это Dog
} else {
animal.meow(); // А здесь — Cat
}
}
const dog = new Dog();
const cat = new Cat();
makeSound(dog); // "Woof! Woof!"
makeSound(cat); // "Meow! Meow!"
in — для проверки свойств объекта
Иногда мы работаем не с классами, а с обычными объектами, у которых есть разные свойства. Оператор in проверяет наличие свойства в объекте:
type Fish = {
swim: () => void;
name: string;
};
type Bird = {
fly: () => void;
name: string;
};
function move(animal: Fish | Bird): void {
if ("swim" in animal) {
animal.swim(); // TypeScript знает: это Fish
} else {
animal.fly(); // А это Bird
}
}
const fish: Fish = {
swim: () => console.log("Swimming..."),
name: "Nemo"
};
const bird: Bird = {
fly: () => console.log("Flying..."),
name: "Tweety"
};
Пользовательские Type Guards
Иногда встроенных проверок недостаточно. TypeScript позволяет создавать собственные функции-защитники с помощью синтаксиса value is Type:
// Определяем пользовательский Type Guard
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isNumber(value: unknown): value is number {
return typeof value === "number";
}
function processValue(value: string | number | boolean): string | number {
if (isString(value)) {
return value.toUpperCase();
}
if (isNumber(value)) {
return value * 2;
}
return "Булево значение: " + value;
}
console.log(processValue("hello")); // "HELLO"
console.log(processValue(10)); // 20
console.log(processValue(true)); // "Булево значение: true"
Пользовательские Type Guards особенно полезны для сложных проверок, например, для объектов с определённой структурой:
interface User {
name: string;
age: number;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
"age" in value &&
typeof (value as User).name === "string" &&
typeof (value as User).age === "number"
);
}
function processData(data: string | number | User): string {
if (isUser(data)) {
return `Пользователь: ${data.name}, ${data.age} лет`;
}
if (typeof data === "string") {
return `Строка: ${data}`;
}
return `Число: ${data}`;
}
Type Aliases: даём имена типам
Когда типы становятся сложными, их неудобно повторять каждый раз. Type Aliases приходят на помощь:
// Вместо того чтобы писать это каждый раз
let userId: string | number = 123;
// Мы создаём псевдоним
type ID = string | number;
let userId: ID = 123;
let productId: ID = "abc";
let orderId: ID = 456;
// Это работает и для более сложных типов
type Status = "active" | "inactive" | "pending";
type User = {
id: ID;
name: string;
status: Status;
};
const user: User = {
id: "user-001",
name: "Alice",
status: "active"
};
Оживи свой сайт. Освой JavaScript!
Статичная верстка — это только скелет. Наш онлайн-курс "JavaScript с нуля до профи" даст твоим страницам мышцы и нервы. Научись создавать слайдеры, формы, интерактивные карты и получать данные с сервера.
От теории — к реальным скриптам в твоём портфолио.
Псевдонимы для Union Types
Type Aliases особенно полезны для Union Types, которые используются в нескольких местах:
// Определяем типы фигур
type Circle = {
kind: "circle";
radius: number;
};
type Square = {
kind: "square";
side: number;
};
type Triangle = {
kind: "triangle";
base: number;
height: number;
};
// Создаём псевдоним для объединения
type Shape = Circle | Square | Triangle;
// Используем в разных функциях
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
function getDescription(shape: Shape): string {
switch (shape.kind) {
case "circle":
return `Круг радиусом ${shape.radius}`;
case "square":
return `Квадрат со стороной ${shape.side}`;
case "triangle":
return `Треугольник с основанием ${shape.base} и высотой ${shape.height}`;
}
}
// Создаём массив фигур
const shapes: Shape[] = [
{ kind: "circle", radius: 5 },
{ kind: "square", side: 4 },
{ kind: "triangle", base: 3, height: 6 }
];
Композиция Type Aliases
Type Aliases можно комбинировать и расширять:
// Базовый тип
type BaseEntity = {
id: number;
createdAt: Date;
updatedAt: Date;
};
// Расширяем базовый тип
type User = BaseEntity & {
name: string;
email: string;
isActive: boolean;
};
type Product = BaseEntity & {
name: string;
price: number;
inStock: boolean;
};
const user: User = {
id: 1,
createdAt: new Date(),
updatedAt: new Date(),
name: "Alice",
email: "alice@example.com",
isActive: true
};
const product: Product = {
id: 1,
createdAt: new Date(),
updatedAt: new Date(),
name: "Laptop",
price: 999.99,
inStock: true
};
Дискриминирующие объединения (Discriminated Unions)
Это один из самых мощных паттернов в TypeScript. Когда у всех типов в объединении есть общее свойство-дискриминатор, TypeScript может автоматически сужать типы:
// Дискриминатор — свойство 'type'
type SuccessResponse = {
type: "success";
data: any;
status: 200;
};
type ErrorResponse = {
type: "error";
message: string;
status: 400 | 401 | 403 | 404 | 500;
};
type LoadingResponse = {
type: "loading";
progress: number;
};
type ApiResponse = SuccessResponse | ErrorResponse | LoadingResponse;
function handleResponse(response: ApiResponse): string {
// TypeScript автоматически сужает тип на основе значения 'type'
switch (response.type) {
case "success":
return `Успех! Данные: ${JSON.stringify(response.data)}`;
case "error":
return `Ошибка ${response.status}: ${response.message}`;
case "loading":
return `Загрузка: ${response.progress}%`;
}
}
const success: ApiResponse = {
type: "success",
data: { user: "Alice" },
status: 200
};
const error: ApiResponse = {
type: "error",
message: "Не найдено",
status: 404
};
const loading: ApiResponse = {
type: "loading",
progress: 75
};
Практическое упражнение
Теперь ваша очередь применить все изученные инструменты.
Задание: Создайте систему обработки платежей:
Определите три типа платежей:
CardPayment:method: "card",cardNumber: string,expiry: string,cvv: stringPayPalPayment:method: "paypal",email: string,token: stringCryptoPayment:method: "crypto",wallet: string,currency: "BTC" | "ETH" | "USDT"
Создайте Union Type Payment из этих трёх типов.
Напишите функцию processPayment, которая:
- Принимает платеж и сумму (число)
- Возвращает строку с результатом обработки
- Использует дискриминатор
methodдля сужения типа - Для каждого типа генерирует разное сообщение
Напишите Type Guard isValidPayment, который проверяет, что у объекта есть все необходимые поля.
Создайте массив платежей и обработайте их.
Решение (не подглядывайте сразу!):
// 1. Типы платежей
type CardPayment = {
method: "card";
cardNumber: string;
expiry: string;
cvv: string;
};
type PayPalPayment = {
method: "paypal";
email: string;
token: string;
};
type CryptoPayment = {
method: "crypto";
wallet: string;
currency: "BTC" | "ETH" | "USDT";
};
// 2. Union Type
type Payment = CardPayment | PayPalPayment | CryptoPayment;
// 3. Функция обработки
function processPayment(payment: Payment, amount: number): string {
// Используем дискриминатор 'method' для сужения типа
switch (payment.method) {
case "card":
return `💳 Оплата картой ****${payment.cardNumber.slice(-4)} на сумму $${amount} обработана (срок: ${payment.expiry})`;
case "paypal":
return `💰 Оплата через PayPal (${payment.email}) на сумму $${amount} обработана (токен: ${payment.token})`;
case "crypto":
return `₿ Крипто-оплата кошельком ${payment.wallet} в ${payment.currency} на сумму $${amount} обработана`;
}
}
// 4. Type Guard для проверки валидности платежа
function isValidCardPayment(value: unknown): value is CardPayment {
return (
typeof value === "object" &&
value !== null &&
"method" in value &&
value.method === "card" &&
"cardNumber" in value &&
"expiry" in value &&
"cvv" in value
);
}
function isValidPayment(value: unknown): value is Payment {
if (typeof value !== "object" || value === null) return false;
const payment = value as any;
if (!("method" in payment)) return false;
switch (payment.method) {
case "card":
return "cardNumber" in payment && "expiry" in payment && "cvv" in payment;
case "paypal":
return "email" in payment && "token" in payment;
case "crypto":
return "wallet" in payment && "currency" in payment;
default:
return false;
}
}
// 5. Массив платежей
const payments: Payment[] = [
{
method: "card",
cardNumber: "4111111111111111",
expiry: "12/25",
cvv: "123"
},
{
method: "paypal",
email: "user@example.com",
token: "paypal-token-123"
},
{
method: "crypto",
wallet: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
currency: "ETH"
}
];
// Обрабатываем платежи
payments.forEach((payment, index) => {
const amount = (index + 1) * 100;
console.log(processPayment(payment, amount));
});
// Проверяем валидность
const invalidData = { method: "card", cardNumber: "4111" };
console.log(`Платеж валиден: ${isValidPayment(invalidData)}`); // false
console.log(`Платеж валиден: ${isValidPayment(payments[0])}`); // true
/* Вывод:
💳 Оплата картой ****1111 на сумму $100 обработана (срок: 12/25)
💰 Оплата через PayPal (user@example.com) на сумму $200 обработана (токен: paypal-token-123)
₿ Крипто-оплата кошельком 0x742d35Cc6634C0532925a3b844Bc454e4438f44e в ETH на сумму $300 обработана
*/
Итог
Сегодня мы освоили три ключевых инструмента TypeScript:
Type Narrowing — процесс сужения широкого типа до более конкретного с помощью условных операторов.
Type Guards — механизмы проверки типов:
typeofдля примитивовinstanceofдля классовinдля проверки свойств- Пользовательские Guard-функции с синтаксисом
value is Type
Type Aliases — создание переиспользуемых имен для типов:
- Упрощение сложных типов
- Композиция типов с помощью &
- Дискриминирующие объединения
Теперь у вас есть все инструменты для работы с Union Types. Вы можете создавать гибкие, но безопасные типы и уверенно работать с ними, зная, что TypeScript поможет вам избежать ошибок.
В следующем уроке мы перейдём к практике и разберём, как мигрировать существующий JavaScript-код на TypeScript, сохраняя при этом его работоспособность.