- Главная
- Typescript
- Union Types в Typescript — гибкость под контролем
Union Types в Typescript — гибкость под контролем
В предыдущих уроках мы научились строго определять типы для переменных, массивов и объектов. Но реальный мир редко бывает чёрно-белым. Иногда функция должна уметь принимать разные типы данных, и это нормально. JavaScript славится своей гибкостью, и TypeScript не отбирает эту гибкость — он просто делает её безопасной.
Именно здесь на помощь приходят Union Types (объединения типов). Это мощный инструмент, который позволяет вам сказать: «Эта переменная может быть либо строкой, либо числом» — и при этом сохранить контроль над типами.
Что такое 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 идеально подходят для:
- API-ответов — когда данные могут приходить в разных форматах.
- Обработки ошибок — когда функция может вернуть либо результат, либо ошибку.
- Пользовательского ввода — когда данные приходят из форм или внешних источников.
- Гибких функций — когда функция должна работать с разными типами данных.
Практическое упражнение
Теперь ваша очередь применить знания на практике.
Задание. Напишите TypeScript-код для системы управления событиями:
1. Создайте типы для трёх видов событий:
Meeting: имеетtitle,date(строка),duration(число в часах),attendees(массив строк)Deadline: имеетtitle,date(строка),priority("high" | "medium" | "low"),completed(булево)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:
- Синтаксис — вертикальная черта
|для объединения типов. - Примитивы и литералы — создание строгих перечислений с помощью литеральных типов.
- Объекты — описание структур, которые могут быть разных видов.
- Дискриминирующие объединения — использование общего свойства для определения конкретного типа.
- Массивы — создание массивов с элементами разных типов.
- Ограничения — доступны только общие для всех типов методы и свойства.
Union Types делают TypeScript по-настоящему гибким. Вы можете работать с разнородными данными, но при этом сохранять контроль над типами. Однако с гибкостью приходит и ответственность — теперь мы должны научиться правильно сужать эти типы, чтобы безопасно использовать их специфические методы и свойства.
В следующем уроке мы погрузимся в Type Narrowing, Type Guards и Type Aliases — инструменты, которые позволят нам полностью контролировать Union Types и делать код ещё более надёжным.