Пошук уроків, статей та іншого контенту
Описуємо альтернативні форми даних за допомогою union types та звужуємо їх у коді.
Union type описує значення, яке може мати один із кількох типів. Для об’єднання типів використовується оператор |.
let identifier: string | number;
identifier = "user-42";
identifier = 42;
// identifier = true; // Помилка: boolean не входить до union typeТип string | number означає: у змінній буде або string, або number. TypeScript не знає наперед, який саме варіант використовується в кожен момент.
Union type можна застосовувати до:
змінних;
параметрів функцій;
значень, які повертають функції;
властивостей об’єктів;
елементів масивів.
type Status = "pending" | "success" | "error";
function printStatus(status: Status): void {
console.log(status);
}
printStatus("pending");
printStatus("success");
// printStatus("cancelled"); // ПомилкаУ цьому прикладі Status — це union не загальних типів, а конкретних рядкових значень. Такий тип називають літеральним union type.
Якщо значення має кілька можливих типів, TypeScript дозволяє виконувати лише операції, спільні для всіх варіантів.
function printValue(value: string | number): void {
console.log(value.toString());
}Метод toString() доступний і для рядка, і для числа, тому код коректний.
Натомість операція, специфічна для одного варіанта, потребує перевірки:
function formatValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}Після перевірки typeof value === "string" TypeScript розуміє, що всередині блоку value має тип string. Після завершення цього блоку залишається варіант number.
Процес уточнення типу називається звуженням типу (type narrowing).
typeofОператор typeof підходить для базових типів JavaScript:
"string";
"number";
"boolean";
"bigint";
"symbol";
"undefined";
"function";
"object".
function describe(value: string | number | boolean): string {
if (typeof value === "string") {
return `Рядок із довжиною ${value.length}`;
}
if (typeof value === "number") {
return `Число: ${value}`;
}
return `Булеве значення: ${value}`;
}
console.log(describe("TypeScript"));
console.log(describe(42));
console.log(describe(true));Перевірки можна комбінувати:
function add(a: string | number, b: string | number): string | number {
if (typeof a === "number" && typeof b === "number") {
return a + b;
}
return `${a}${b}`;
}
console.log(add(2, 3)); // 5
console.log(add("Hello, ", "world")); // Hello, world
console.log(add("Версія ", 5)); // Версія 5Зверніть увагу: результат a + b залежить від типів обох операндів. Якщо обидва значення є числами, виконується додавання. В інших випадках ми явно формуємо рядок.
Для union type з об’єктів часто використовують оператор in. Він перевіряє, чи є властивість в об’єкті.
type EmailNotification = {
email: string;
subject: string;
};
type SmsNotification = {
phone: string;
message: string;
};
type Notification = EmailNotification | SmsNotification;
function sendNotification(notification: Notification): void {
if ("email" in notification) {
console.log(`Надсилаємо email на ${notification.email}`);
console.log(`Тема: ${notification.subject}`);
} else {
console.log(`Надсилаємо SMS на ${notification.phone}`);
console.log(`Текст: ${notification.message}`);
}
}
sendNotification({
email: "user@example.com",
subject: "Оновлення профілю",
});
sendNotification({
phone: "+380501234567",
message: "Ваш код підтвердження: 1234",
});TypeScript визначає:
якщо властивість email існує, об’єкт має тип EmailNotification;
в іншому випадку для цього union залишаються властивості SmsNotification.
Такий підхід добре працює, коли альтернативні об’єкти мають різні обов’язкові властивості.
Надійнішим підходом для об’єктів є спільна властивість-дискримінатор. Її значення показує, який саме варіант об’єкта використовується.
type LoadingState = {
status: "loading";
};
type SuccessState = {
status: "success";
data: string[];
};
type ErrorState = {
status: "error";
message: string;
};
type RequestState = LoadingState | SuccessState | ErrorState;
function renderState(state: RequestState): string {
switch (state.status) {
case "loading":
return "Завантаження...";
case "success":
return `Отримано записів: ${state.data.length}`;
case "error":
return `Помилка: ${state.message}`;
}
}
console.log(renderState({ status: "loading" }));
console.log(
renderState({
status: "success",
data: ["TypeScript", "JavaScript"],
}),
);
console.log(
renderState({
status: "error",
message: "Сервер недоступний",
}),
);Властивість status називають дискримінатором. Коли status має значення "success", TypeScript дозволяє звертатися до data. Коли значення "error" — до message.
Це особливо корисно для опису:
станів HTTP-запиту;
подій;
результатів операцій;
повідомлень між частинами програми;
різних форм конфігурації.
Union type допомагає точно описати функцію, яка приймає кілька форматів даних.
type UserId = string | number;
function getUserUrl(id: UserId): string {
if (typeof id === "number") {
return `/users/${id}`;
}
return `/users/${encodeURIComponent(id)}`;
}
console.log(getUserUrl(42));
console.log(getUserUrl("admin-user"));Важливо відрізняти union параметрів від кількох окремих сигнатур функції. У простих випадках union достатній і зрозуміліший:
function normalize(value: string | number): string {
return String(value).trim();
}Функція гарантує, що отримає рядок або число, а всередині може працювати із загальним для них результатом String(value).
Якщо кілька типів у union мають спільну властивість, нею можна користуватися без додаткової перевірки.
type Admin = {
name: string;
permissions: string[];
};
type Customer = {
name: string;
ordersCount: number;
};
type User = Admin | Customer;
function getUserName(user: User): string {
return user.name;
}Властивість name є в обох варіантах, тому TypeScript дозволяє звертатися до неї одразу.
Але специфічні властивості потребують звуження:
function describeUser(user: User): string {
if ("permissions" in user) {
return `${user.name}: дозволів ${user.permissions.length}`;
}
return `${user.name}: замовлень ${user.ordersCount}`;
}Іноді одна й та сама перевірка потрібна в кількох місцях. Її можна винести у функцію-предикат із type predicate.
type Circle = {
kind: "circle";
radius: number;
};
type Rectangle = {
kind: "rectangle";
width: number;
height: number;
};
type Shape = Circle | Rectangle;
function isCircle(shape: Shape): shape is Circle {
return shape.kind === "circle";
}
function getArea(shape: Shape): number {
if (isCircle(shape)) {
return Math.PI * shape.radius ** 2;
}
return shape.width * shape.height;
}
console.log(getArea({ kind: "circle", radius: 3 }));
console.log(getArea({ kind: "rectangle", width: 4, height: 5 }));Запис shape is Circle повідомляє TypeScript: якщо функція повернула true, параметр shape можна розглядати як Circle.
Без type predicate TypeScript знав би лише, що функція повернула boolean, але не зміг би використати результат для звуження типу.
Для дискримінованих union type бажано обробляти кожен можливий варіант. Це захищає код від ситуації, коли до типу додали новий варіант, але забули оновити функцію.
Для цього можна використати тип never:
type Circle = {
kind: "circle";
radius: number;
};
type Rectangle = {
kind: "rectangle";
width: number;
height: number;
};
type Triangle = {
kind: "triangle";
base: number;
height: number;
};
type Shape = Circle | Rectangle | Triangle;
function assertNever(value: never): never {
throw new Error(`Невідомий тип фігури: ${JSON.stringify(value)}`);
}
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
return assertNever(shape);
}
}Коли всі варіанти Shape оброблено, у блоці default shape має тип never. Якщо додати новий варіант і не додати для нього case, TypeScript повідомить про помилку під час передавання значення в assertNever.
Це називають вичерпною перевіркою (exhaustiveness checking).
Union type означає «один із варіантів»:
type Id = string | number;Значення Id є або рядком, або числом.
Intersection type, який позначається &, означає «одночасно всі типи»:
type HasName = {
name: string;
};
type HasEmail = {
email: string;
};
type Contact = HasName & HasEmail;
const contact: Contact = {
name: "Олена",
email: "olena@example.com",
};Для опису альтернативних форм даних використовуйте |, а не &.
type FileResult = {
type: "file";
path: string;
};
type UrlResult = {
type: "url";
url: string;
};
type Result = FileResult | UrlResult;
function getLocation(result: Result): string {
if (result.type === "file") {
return result.path;
}
return result.url;
}Без перевірки result.type властивість path або url недоступна, оскільки вона існує лише в одному з варіантів.
Не варто замінювати звуження безпідставним as:
const value: string | number = 42;
// Небезпечно: фактичне значення може бути не рядком
const text = value as string;Таке приведення типу не змінює значення під час виконання і не перевіряє його. Краще виконати реальну перевірку:
function toUpperCase(value: string | number): string {
if (typeof value !== "string") {
return String(value);
}
return value.toUpperCase();
}Дискримінатор має однозначно розділяти варіанти:
type Draft = {
status: "draft";
content: string;
};
type Published = {
status: "published";
content: string;
publishedAt: Date;
};
type Article = Draft | Published;Якщо всі варіанти мають однакове значення status, ця властивість не допоможе звузити тип.
switchЯкщо union містить кілька варіантів, обробляйте їх усі. Вичерпна перевірка з never допомагає виявити пропущений варіант ще під час компіляції.
Union type описує значення, яке може мати один із кількох типів.
Для створення union використовується оператор |.
Літеральні union типи обмежують значення конкретним набором варіантів.
Щоб працювати зі специфічними властивостями, потрібно звужувати тип.
Для звуження використовують typeof, оператор in, перевірки значень і дискриміновані властивості.
Спільна властивість доступна для всіх варіантів union без додаткової перевірки.
Type predicate дає змогу винести перевірку типу в окрему функцію.
never допомагає перевірити, що всі варіанти union оброблено.