Посібник по об’єднаннях (Unions) та перетинах (Intersections) в TypeScript

pic

Фото: ThisisEngineering на Unsplash

Типова система TypeScript потужна та гнучка, завдяки своїй здатності комбінувати типи різними способами. Два з найважливіших операторів типів — це об'єднання (|) та перетини (&). Хоча на перший погляд вони можуть здатися схожими, розуміння їхніх відмінностей і вміння правильно використовувати кожен з них є ключовим для написання типобезпечного коду. Давайте детальніше розглянемо обидва поняття на практичних прикладах.

Розуміння основ

Об'єднання типів (|)

Об'єднання типів означає, що значення може бути одним з кількох типів. Це можна розглядати як операцію "АБО". Наприклад, значення може бути типу A АБО типу B.

type StringOrNumber = string | number;  
let value: StringOrNumber;  
value = "Hello"; // Дійсно  
value = 42; // Дійсно  
value = true; // Помилка: Тип 'boolean' не можна присвоїти

Перетини типів (&)

Перетин типів поєднує кілька типів в один. Це можна розглядати як операцію "І". Отриманий тип має всі властивості всіх складових типів.

type HasName = { name: string };  
type HasAge = { age: number };  

type Person = HasName & HasAge;  

const person: Person = {  
 name: "John",  
 age: 30  
}; // Дійсно  

const incomplete: Person = {  
 name: "John"  
}; // Помилка: Відсутня властивість 'age'

Приклади з реального світу

1. Обробка відповідей API

Об'єднання типів ідеально підходять для обробки різних станів відповіді API:

type ApiResponse = {  
 status: "loading";  
} | {  
 status: "success";  
 data: T;  
} | {  
 status: "error";  
 error: string;  
};  

function handleResponse(response: ApiResponse) {  
 switch (response.status) {  
 case "loading":  
 showSpinner();  
 break;  
 case "success":  
 displayUser(response.data); // TypeScript знає, що data існує тут  
 break;  
 case "error":  
 showError(response.error); // TypeScript знає, що error існує тут  
 break;  
 }  
}

2. Пропси компонентів з рольовими дозволами

Перетини типів чудово підходять для поєднання різних наборів пропсів:

type BaseProps = {  
 className?: string;  
 style?: React.CSSProperties;  
};  

type AdminFeatures = {  
 canDelete: boolean;  
 canEdit: boolean;  
};  

type UserFeatures = {  
 canView: boolean;  
 canComment: boolean;  
};  

type AdminComponent = BaseProps & AdminFeatures;  
type UserComponent = BaseProps & UserFeatures;  

const AdminPanel = (props: AdminComponent) => {  
 return (  
    {props.canDelete && Delete}    {props.canEdit && Edit}    
    );  
}; 

3. Стани валідації форм

Об'єднання типів чудово підходять для представлення взаємно виключних станів:

type ValidationState = {  
 status: "valid";  
 value: string;  
} | {  
 status: "invalid";  
 errors: string[];  
} | {  
 status: "pending";  
};  

function validateForm(state: ValidationState) {  
 if (state.status === "valid") {  
 submitForm(state.value);  
 } else if (state.status === "invalid") {  
 displayErrors(state.errors);  
 } else {  
 showLoadingIndicator();  
 }  
}  

## Об'єкти конфігурації з дефолтними значеннями

Перетини типів ідеально підходять для розширення об'єктів конфігурації.

type DefaultConfig = {
timeout: number;
retries: number;
};

type UserConfig = Partial;

type Config = DefaultConfig & UserConfig;

const defaultConfig: DefaultConfig = {
timeout: 3000,
retries: 3
};

function createConfig(userConfig: UserConfig): Config {
return { ...defaultConfig, ...userConfig };
}
```

Розширені патерни

Дискриміновані об'єднання (Discriminated Unions)

Потужний патерн, який поєднує об'єднання з літеральними типами:

type Shape =   
 | { kind: "circle"; radius: number }  
 | { kind: "rectangle"; width: number; height: number }  
 | { kind: "triangle"; base: number; height: number };  

function calculateArea(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;  
 }  
}

Патерн Mixin з перетинами (Mixin Pattern with Intersections)

Створення гнучких, комбінованих типів:

type Timestamped<T> = T & {  
 createdAt: Date;  
 updatedAt: Date;  
};  

type Identifiable<T> = T & {  
 id: string;  
};  

type Trackable<T> = T & {  
 version: number;  
};  

// Поєднання кількох Mixin  
type Document = {  
 content: string;  
};  

type TrackedDocument = Timestamped<Identifiable<Trackable<Document>>>;  

const doc: TrackedDocument = {  
 content: "Hello",  
 id: "123",  
 version: 1,  
 createdAt: new Date(),  
 updatedAt: new Date()  
};

Кращі практики

Використовуйте об'єднання (Unions), коли:

  • Ви маєте взаємно виключні стани
  • Значення може бути одним з кількох типів
  • Вам потрібно звуження типів
  • Працюєте з дискримінованими об'єднаннями (Discriminated Unions)

Використовуйте перетини (Intersections), коли:

  • Поєднуєте кілька типів в один
  • Створюєте Mixin
  • Розширюєте існуючі типи
  • Будуєте комбіновані типові структури

Уникайте поширених проблем:

  • Не зловживайте об'єднаннями (Unions), коли замість них краще підійдуть перелічення (enums)
  • Будьте обережні з розширенням типів у об'єднаннях (Unions)
  • Слідкуйте за типом never у некоректних перетинах (Intersections)
  • Оцініть читаємість при вкладенні кількох перетинів (Intersections)

Висновок

Розуміння того, коли використовувати об'єднання (Unions) або перетини (Intersections), є важливим для написання підтримуваного коду TypeScript. Об'єднання чудово підходять для обробки варіативних типів та управління станами, тоді як перетини ідеально підходять для поєднання та розширення типів. Правильне використання цих інструментів дозволить створювати більш виразні та типобезпечні додатки.

Ключове завдання — зрозуміти, чи маєте справу з ситуацією "АБО" (об'єднання) або з ситуацією "І" (перетини). З практикою вибір між ними стане природнім, що призведе до більш надійного та підтримуваного коду.

Перекладено з: TypeScript Unions vs Intersections: A Practical Guide

Leave a Reply

Your email address will not be published. Required fields are marked *