Хотите заказать веб-сайт? Связаться с нами

Type Narrowing, Guards и Aliases — контроль над типами в Typescript

В прошлом уроке мы научились создавать Union Types, которые позволяют переменным принимать значения разных типов. Это даёт нам гибкость, но создаёт новую проблему: как работать с переменной, которая может быть строкой, числом или объектом? Ведь у каждого типа свои методы и свойства.

Сегодня мы разберём три мощных инструмента, которые решают эту задачу:

  1. Type Narrowing — процесс сужения широкого типа до более конкретного.
  2. Type Guards — механизмы, которые помогают TypeScript понять, с каким именно типом мы работаем.
  3. Type Aliases — способ дать имена сложным типам для переиспользования.
Type Narrowing, Guards и Aliases — контроль над типами в Typescript

Проблема: 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
};

Практическое упражнение

Теперь ваша очередь применить все изученные инструменты.

Задание: Создайте систему обработки платежей:

Определите три типа платежей:

  1. CardPayment: method: "card", cardNumber: string, expiry: string, cvv: string
  2. PayPalPayment: method: "paypal", email: string, token: string
  3. CryptoPayment: method: "crypto", wallet: string, currency: "BTC" | "ETH" | "USDT"

Создайте Union Type Payment из этих трёх типов.

Напишите функцию processPayment, которая:

  1. Принимает платеж и сумму (число)
  2. Возвращает строку с результатом обработки
  3. Использует дискриминатор method для сужения типа
  4. Для каждого типа генерирует разное сообщение

Напишите 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 — механизмы проверки типов:

  1. typeof для примитивов
  2. instanceof для классов
  3. in для проверки свойств
  4. Пользовательские Guard-функции с синтаксисом value is Type

Type Aliases — создание переиспользуемых имен для типов:

  1. Упрощение сложных типов
  2. Композиция типов с помощью &
  3. Дискриминирующие объединения

Теперь у вас есть все инструменты для работы с Union Types. Вы можете создавать гибкие, но безопасные типы и уверенно работать с ними, зная, что TypeScript поможет вам избежать ошибок.

В следующем уроке мы перейдём к практике и разберём, как мигрировать существующий JavaScript-код на TypeScript, сохраняя при этом его работоспособность.

Теги: