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

Union Types в Typescript — гибкость под контролем

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

Именно здесь на помощь приходят Union Types (объединения типов). Это мощный инструмент, который позволяет вам сказать: «Эта переменная может быть либо строкой, либо числом» — и при этом сохранить контроль над типами.

Union Types в Typescript — гибкость под контролем

Что такое Union Types?

Union Type записывается через вертикальную черту | и означает «или». Это способ объединить несколько типов в один, создавая более широкий тип.

// Переменная может быть либо строкой, либо числом
let id: string | number;
id = "abc123"; // ✅
id = 456; // ✅
id = true; // ❌ Ошибка: boolean не входит в объединение

Это невероятно полезно для практических сценариев. Например, ID пользователя в базе данных может быть как числом, так и строкой (в зависимости от системы). Или функция может принимать аргумент, который приходит из внешнего API — и вы точно не знаете, число там или строка.

Union Types с примитивами

Самый простой случай — объединение примитивных типов:

type StringOrNumber = string | number;
type BoolOrString = boolean | string;

let value1: StringOrNumber = "hello"; // ✅
let value2: StringOrNumber = 42; // ✅
let value3: BoolOrString = true; // ✅
let value4: BoolOrString = "yes"; // ✅

Union Types с литералами

Сочетание Union Types и литеральных типов даёт нам возможность создавать строгие перечисления допустимых значений:

type Direction = "up" | "down" | "left" | "right";
type Status = "pending" | "approved" | "rejected";

function move(direction: Direction): void {
  console.log(`Moving ${direction}`);
}

function updateStatus(status: Status): void {
  console.log(`Status updated to: ${status}`);
}

move("up"); // ✅
move("north"); // ❌ Ошибка: 'north' не входит в тип Direction
updateStatus("pending"); // ✅
updateStatus("cancelled"); // ❌ Ошибка

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

Union Types с объектами

Union Types работают не только с примитивами, но и со сложными структурами:

type Admin = {
  name: string;
  permissions: string[];
  isAdmin: true;
};

type User = {
  name: string;
  email: string;
  isAdmin: false;
};

type Account = Admin | User;

function getAccountInfo(account: Account): string {
  if (account.isAdmin) {
    // TypeScript знает, что здесь account — это Admin
    return `Admin ${account.name} has permissions: ${account.permissions.join(", ")}`;
  } else {
    // А здесь — User
    return `User ${account.name} with email ${account.email}`;
  }
}

const admin: Admin = {
  name: "Alice",
  permissions: ["read", "write", "delete"],
  isAdmin: true
};

const user: User = {
  name: "Bob",
  email: "bob@example.com",
  isAdmin: false
};

console.log(getAccountInfo(admin)); // "Admin Alice has permissions: read, write, delete"
console.log(getAccountInfo(user)); // "User Bob with email bob@example.com"

Обратите внимание: внутри функции мы используем проверку account.isAdmin, чтобы определить, с каким типом мы работаем. Это называется Type Guard (защитник типа) — мы подробно разберём их в следующем уроке.

Из ноутбука появляются летающие светящиеся экраны

Оживи свой сайт. Освой JavaScript!

Статичная верстка — это только скелет. Наш онлайн-курс "JavaScript с нуля до профи" даст твоим страницам мышцы и нервы. Научись создавать слайдеры, формы, интерактивные карты и получать данные с сервера.

От теории — к реальным скриптам в твоём портфолио.

Подробнее о курсе

Union Types с опциональными свойствами

Union Types особенно полезны в сочетании с опциональными свойствами, когда вы точно не знаете, какая структура данных придёт:

type Product = {
  id: number;
  name: string;
  price: number;
  inStock?: boolean; // опционально
};

type Service = {
  id: number;
  name: string;
  hourlyRate: number;
  hours: number;
};

type Item = Product | Service;

function calculatePrice(item: Item): number {
  if ("price" in item) {
    // Это Product
    return item.price;
  } else {
    // Это Service
    return item.hourlyRate * item.hours;
  }
}

const laptop: Product = { id: 1, name: "Laptop", price: 999.99 };
const consulting: Service = { id: 2, name: "Consulting", hourlyRate: 100, hours: 5 };

console.log(calculatePrice(laptop)); // 999.99
console.log(calculatePrice(consulting)); // 500

Union Types с массивами

Мы уже видели, что Union Types можно использовать с массивами:

// Массив, который может содержать строки или числа
let mixedArray: (string | number)[] = ["Alice", 25, "Bob", 30];

// Массив объектов разных типов
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Shape = Circle | Square;

const shapes: Shape[] = [
  { kind: "circle", radius: 10 },
  { kind: "square", side: 5 },
  { kind: "circle", radius: 7 }
];

function getArea(shape: Shape): number {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
  } else {
    return shape.side ** 2;
  }
}

shapes.forEach(shape => {
  console.log(`Area: ${getArea(shape)}`);
});

Здесь мы используем дискриминирующее объединение (discriminated union) — это когда у всех типов в объединении есть общее свойство (в примере — kind), по которому можно определить конкретный тип.

Типизация переменных до присвоения

Важный момент: когда вы объявляете переменную с Union Type, но не присваиваете ей значение сразу, TypeScript требует, чтобы вы точно указали тип:

// TypeScript не может вывести тип, потому что значение ещё не присвоено
let value: string | number;
// value = "hello"; // ✅
// value = 42; // ✅

// Если вы попытаетесь использовать value до присвоения — ошибка
// console.log(value); // ❌ Ошибка: Variable 'value' is used before being assigned

В строгом режиме TypeScript будет следить, чтобы вы не использовали переменную, пока ей не присвоено значение, которое соответствует хотя бы одному из типов объединения.

Ограничения Union Types

Когда вы работаете с Union Type, вы можете использовать только те методы и свойства, которые общие для всех типов в объединении:

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, которые мы подробно разберём в следующем уроке.

Когда использовать Union Types?

Union Types идеально подходят для:

  1. API-ответов — когда данные могут приходить в разных форматах.
  2. Обработки ошибок — когда функция может вернуть либо результат, либо ошибку.
  3. Пользовательского ввода — когда данные приходят из форм или внешних источников.
  4. Гибких функций — когда функция должна работать с разными типами данных.

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

Теперь ваша очередь применить знания на практике.

Задание. Напишите TypeScript-код для системы управления событиями:

1. Создайте типы для трёх видов событий:

  1. Meeting: имеет title, date (строка), duration (число в часах), attendees (массив строк)
  2. Deadline: имеет title, date (строка), priority ("high" | "medium" | "low"), completed (булево)
  3. Reminder: имеет title, date (строка), message (строка), sent (булево)

2. Создайте объединенный тип из этих трёх типов.

3. Напишите функцию getEventSummary, которая принимает событие и возвращает строку с кратким описанием. Для каждого типа события возвращайте разное описание.

4. Создайте массив событий разных типов и выведите их сводки.

Решение (не подглядывайте сразу!):

// 1. Типы событий
type Meeting = {
  title: string;
  date: string;
  duration: number;
  attendees: string[];
};

type Deadline = {
  title: string;
  date: string;
  priority: "high" | "medium" | "low";
  completed: boolean;
};

type Reminder = {
  title: string;
  date: string;
  message: string;
  sent: boolean;
};

// 2. Union Type для событий
type Event = Meeting | Deadline | Reminder;

// 3. Функция для получения сводки
function getEventSummary(event: Event): string {
  // Используем дискриминатор для определения типа
  // У каждого типа есть уникальные свойства, по которым мы их различаем
  
  if ("attendees" in event) {
    // Это Meeting
    return `Meeting "${event.title}" on ${event.date} with ${event.attendees.length} attendees (duration: ${event.duration}h)`;
  } else if ("priority" in event) {
    // Это Deadline
    const status = event.completed ? "✅ Completed" : "⏳ Pending";
    return `Deadline "${event.title}" by ${event.date} [${event.priority} priority] - ${status}`;
  } else {
    // Это Reminder
    const status = event.sent ? "📨 Sent" : "📝 Not sent";
    return `Reminder "${event.title}" on ${event.date}: "${event.message}" (${status})`;
  }
}

// 4. Массив событий
const events: Event[] = [
  {
    title: "Team Sync",
    date: "2024-01-15",
    duration: 1,
    attendees: ["Alice", "Bob", "Charlie"]
  },
  {
    title: "Project Submission",
    date: "2024-01-20",
    priority: "high",
    completed: false
  },
  {
    title: "Buy milk",
    date: "2024-01-10",
    message: "Don't forget to buy milk!",
    sent: true
  }
];

// Вывод сводок
events.forEach(event => {
  console.log(getEventSummary(event));
});

/* Вывод:
Meeting "Team Sync" on 2024-01-15 with 3 attendees (duration: 1h)
Deadline "Project Submission" by 2024-01-20 [high priority] - ⏳ Pending
Reminder "Buy milk" on 2024-01-10: "Don't forget to buy milk!" (📨 Sent)
*/

Итог

Сегодня мы детально разобрали Union Types:

  1. Синтаксис — вертикальная черта | для объединения типов.
  2. Примитивы и литералы — создание строгих перечислений с помощью литеральных типов.
  3. Объекты — описание структур, которые могут быть разных видов.
  4. Дискриминирующие объединения — использование общего свойства для определения конкретного типа.
  5. Массивы — создание массивов с элементами разных типов.
  6. Ограничения — доступны только общие для всех типов методы и свойства.

Union Types делают TypeScript по-настоящему гибким. Вы можете работать с разнородными данными, но при этом сохранять контроль над типами. Однако с гибкостью приходит и ответственность — теперь мы должны научиться правильно сужать эти типы, чтобы безопасно использовать их специфические методы и свойства.

В следующем уроке мы погрузимся в Type Narrowing, Type Guards и Type Aliases — инструменты, которые позволят нам полностью контролировать Union Types и делать код ещё более надёжным.

Теги: