Пошук уроків, статей та іншого контенту
Комбінуйте кілька можливих типів через union types та звужуйте їх до конкретного варіанта під час виконання.
Union type описує значення, яке може мати один із кількох типів. Для об’єднання типів використовується оператор |.
let id: string | number;
id = 42;
id = "user-42";Змінна id може містити або string, або number. Інші значення TypeScript відхилить:
let id: string | number = true;
// Помилка: тип boolean не входить до string | numberUnion type не означає, що значення одночасно має всі перелічені типи. Воно має один конкретний тип із допустимих, але до моменту перевірки TypeScript не знає, який саме.
Якщо змінна має union type, TypeScript дозволяє використовувати лише операції, спільні для всіх варіантів.
function printValue(value: string | number): void {
console.log(value.toString());
// value.toUpperCase();
// Помилка: метод toUpperCase існує у string, але не у number
}Метод toString() доступний і для рядків, і для чисел, тому його можна викликати одразу. А toUpperCase() доступний лише для string, тому спочатку потрібно визначити фактичний тип значення.
Процес визначення конкретного типу називається звуженням типу (type narrowing).
typeofОператор typeof повертає рядок із назвою примітивного типу під час виконання:
"string"
"number"
"boolean"
"bigint"
"symbol"
"undefined"
"object"
"function"
TypeScript використовує перевірку typeof для звуження union type.
function formatValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}
console.log(formatValue("typescript")); // TYPESCRIPT
console.log(formatValue(12.5)); // 12.50У блоці if змінна value має тип string. Після if залишається варіант number, тому TypeScript дозволяє викликати toFixed().
function describe(value: string | number | boolean): string {
if (typeof value === "string") {
return `Рядок із довжиною ${value.length}`;
}
if (typeof value === "number") {
return `Число ${value}`;
}
return value ? "true" : "false";
}
console.log(describe("hello"));
console.log(describe(100));
console.log(describe(false));Останній return працює з boolean, оскільки після двох перевірок інших варіантів не залишилося.
Для union type із конкретних значень можна використовувати === або !==.
type Direction = "up" | "down" | "left" | "right";
function move(direction: Direction): string {
if (direction === "up") {
return "Рух угору";
}
if (direction === "down") {
return "Рух униз";
}
if (direction === "left") {
return "Рух ліворуч";
}
return "Рух праворуч";
}
console.log(move("left"));У цьому прикладі Direction — це union не типів, а конкретних строкових значень. Такий тип називають literal union type.
Він корисний, коли змінна має приймати лише визначений набір варіантів:
type Theme = "light" | "dark";
function setTheme(theme: Theme): void {
console.log(`Встановлено тему: ${theme}`);
}
setTheme("dark");
// setTheme("blue");
// Помилка: "blue" не входить до ThemeinОператор in перевіряє, чи існує властивість в об’єкті. TypeScript використовує таку перевірку для звуження типу.
type EmailNotification = {
email: string;
subject: string;
};
type SmsNotification = {
phone: string;
message: string;
};
function sendNotification(
notification: EmailNotification | SmsNotification
): string {
if ("email" in notification) {
return `Email для ${notification.email}: ${notification.subject}`;
}
return `SMS на ${notification.phone}: ${notification.message}`;
}
console.log(
sendNotification({
email: "user@example.com",
subject: "Ваше замовлення готове",
})
);
console.log(
sendNotification({
phone: "+380501112233",
message: "Код підтвердження: 1234",
})
);У першій гілці TypeScript знає, що notification — це EmailNotification, тому доступ до email і subject є безпечним.
Для об’єктів часто використовують спільну властивість із конкретними значеннями. Її називають дискримінатором.
type LoadingState = {
status: "loading";
};
type SuccessState = {
status: "success";
data: string[];
};
type ErrorState = {
status: "error";
error: 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.error}`;
}
}
console.log(renderState({ status: "loading" }));
console.log(
renderState({
status: "success",
data: ["TypeScript", "JavaScript"],
})
);
console.log(
renderState({
status: "error",
error: "Сервер недоступний",
})
);Значення status визначає, які властивості доступні:
для "loading" немає даних або помилки;
для "success" доступне data;
для "error" доступне error.
Такий підхід називають discriminated union — об’єднанням із дискримінатором. Він зручний для опису станів запитів, подій, результатів операцій та інших взаємовиключних варіантів.
Іноді перевірку типу потрібно винести в окрему функцію. Для цього в типі, що повертається, використовують конструкцію value is SomeType.
type User = {
id: number;
name: string;
};
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) {
return false;
}
if (!("id" in value) || !("name" in value)) {
return false;
}
return typeof value.id === "number" && typeof value.name === "string";
}
function getUserName(value: unknown): string {
if (isUser(value)) {
return value.name;
}
return "Невідомий користувач";
}
console.log(getUserName({ id: 1, name: "Олена" }));
console.log(getUserName({ id: "1", name: "Олена" }));Після перевірки isUser(value) TypeScript розуміє, що value має тип User.
Тип unknown часто використовують для значень, тип яких заздалегідь невідомий. На відміну від any, він вимагає перевірити значення перед використанням.
Коли union містить кілька варіантів, корисно перевіряти, що код обробляє їх усі. Для цього можна використати тип never.
type PaymentMethod =
| { kind: "card"; last4: string }
| { kind: "cash" }
| { kind: "bank-transfer"; iban: string };
function assertNever(value: never): never {
throw new Error(`Непідтримуваний варіант: ${JSON.stringify(value)}`);
}
function describePayment(payment: PaymentMethod): string {
switch (payment.kind) {
case "card":
return `Картка **** ${payment.last4}`;
case "cash":
return "Готівка";
case "bank-transfer":
return `Банківський переказ: ${payment.iban}`;
default:
return assertNever(payment);
}
}
console.log(
describePayment({
kind: "card",
last4: "1234",
})
);Якщо до PaymentMethod додати новий варіант і не обробити його в switch, TypeScript повідомить про помилку в assertNever(payment). Це допомагає не пропустити нову гілку після розширення union type.
Union можна використовувати як тип елементів масиву:
const values: Array<string | number> = [
"TypeScript",
2026,
"JavaScript",
42,
];
for (const value of values) {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(0));
}
}Також union може описувати параметри та результат функції:
type Result = string | null;
function findUserName(id: number): Result {
if (id === 1) {
return "Олена";
}
return null;
}
const name = findUserName(1);
if (name !== null) {
console.log(name.toUpperCase());
} else {
console.log("Користувача не знайдено");
}Перевірка name !== null звужує тип із string | null до string.
Union type позначається | і означає «один із типів»:
type Id = string | number;Intersection type позначається & і поєднує вимоги всіх типів:
type HasId = {
id: number;
};
type HasTimestamp = {
createdAt: Date;
};
type Entity = HasId & HasTimestamp;
const entity: Entity = {
id: 1,
createdAt: new Date(),
};Для цієї теми важливо запам’ятати:
A | B — значення може бути A або B;
A & B — значення має відповідати і A, і B.
type Circle = {
kind: "circle";
radius: number;
};
type Square = {
kind: "square";
side: number;
};
function getArea(shape: Circle | Square): number {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
return shape.side ** 2;
}Не можна звертатися до radius або side до перевірки kind, оскільки кожна властивість існує лише в одному з варіантів.
function printLength(value: string | number): void {
if (typeof value === "string") {
console.log(value.length);
}
}Не варто замінювати перевірки безпідставним приведенням типу:
function unsafePrintLength(value: string | number): void {
console.log((value as string).length);
}as string не змінює значення під час виконання і не додає реальної перевірки. Якщо передати число, результат може бути неправильним. Спочатку звужуйте тип через typeof, in, перевірку дискримінатора або власний type predicate.
Такий тип дозволяє забагато варіантів:
let status: string = "success";Якщо допустимі лише конкретні стани, краще описати їх явно:
let status: "loading" | "success" | "error" = "loading";Тоді TypeScript зможе перевіряти помилки під час присвоєння та змусить обробити відомі варіанти.
Union type створюється оператором | і описує одне значення з кількох можливих варіантів.
До звуження можна використовувати лише операції, спільні для всіх варіантів.
Для звуження застосовують typeof, перевірки ===, оператор in та дискримінаторні властивості.
Literal union обмежує значення визначеним набором рядків, чисел або інших літералів.
Discriminated union зручно використовувати для взаємовиключних станів об’єкта.
Власний type predicate має форму value is Type і дозволяє винести перевірку в окрему функцію.
Перевірка через never допомагає переконатися, що оброблено всі варіанти union.