Пошук уроків, статей та іншого контенту
Описуємо узагальнені структури даних через інтерфейси та type aliases і порівнюємо обидва підходи.
Узагальнення, або generics, дають змогу описати структуру один раз і використовувати її з різними типами даних.
Без generics довелося б створювати окремі типи для кожного варіанта:
interface UserResponse {
data: User;
status: number;
}
interface ProductResponse {
data: Product;
status: number;
}Ці структури відрізняються лише типом властивості data. Узагальнений тип усуває дублювання:
interface ApiResponse<T> {
data: T;
status: number;
}Тепер T може бути будь-яким типом:
type User = {
id: number;
name: string;
};
type Product = {
id: number;
title: string;
};
const userResponse: ApiResponse<User> = {
data: {
id: 1,
name: "Олена",
},
status: 200,
};
const productResponse: ApiResponse<Product> = {
data: {
id: 10,
title: "Keyboard",
},
status: 200,
};ApiResponse<User> означає, що властивість data має тип User, а ApiResponse<Product> — що вона має тип Product.
Узагальнений інтерфейс оголошується зі списком параметрів типу після його назви:
interface Storage<T> {
save(value: T): void;
load(): T | undefined;
}Реалізація такого інтерфейсу повинна працювати з одним конкретним типом:
class MemoryStorage<T> implements Storage<T> {
private value: T | undefined;
save(value: T): void {
this.value = value;
}
load(): T | undefined {
return this.value;
}
}
const userStorage = new MemoryStorage<User>();
userStorage.save({
id: 2,
name: "Андрій",
});
const storedUser = userStorage.load();
console.log(storedUser);У цьому прикладі:
MemoryStorage<User> може зберігати лише об’єкти типу User;
MemoryStorage<string> може зберігати лише рядки;
тип результату load() також залежить від T.
const stringStorage = new MemoryStorage<string>();
stringStorage.save("TypeScript");
const value = stringStorage.load();
// value має тип string | undefinedTypeScript перевірить неправильне використання:
// stringStorage.save(42); // Помилка: number не сумісний із stringПараметр типу може використовуватися в кількох властивостях і методах інтерфейсу:
interface Repository<T> {
findById(id: number): T | undefined;
save(entity: T): T;
findAll(): T[];
}Конкретний репозиторій визначає тип сутності:
class UserRepository implements Repository<User> {
private users: User[] = [];
findById(id: number): User | undefined {
return this.users.find((user) => user.id === id);
}
save(user: User): User {
this.users.push(user);
return user;
}
findAll(): User[] {
return [...this.users];
}
}Один і той самий інтерфейс можна реалізувати для різних типів сутностей:
type Order = {
id: number;
total: number;
};
class OrderRepository implements Repository<Order> {
private orders: Order[] = [];
findById(id: number): Order | undefined {
return this.orders.find((order) => order.id === id);
}
save(order: Order): Order {
this.orders.push(order);
return order;
}
findAll(): Order[] {
return [...this.orders];
}
}Type alias також може мати параметри типу:
type ApiResponse<T> = {
data: T;
status: number;
message?: string;
};Використання таке саме, як і для узагальненого інтерфейсу:
const response: ApiResponse<User> = {
data: {
id: 3,
name: "Марія",
},
status: 200,
};Type aliases особливо зручні для опису об’єднань типів, перетинів і складніших комбінацій.
type LoadingState = {
status: "loading";
};
type SuccessState<T> = {
status: "success";
data: T;
};
type ErrorState = {
status: "error";
error: string;
};
type RequestState<T> =
| LoadingState
| SuccessState<T>
| ErrorState;Тепер функція може працювати з усіма станами запиту:
function describeState(state: RequestState<User>): string {
switch (state.status) {
case "loading":
return "Запит виконується";
case "success":
return `Користувач: ${state.data.name}`;
case "error":
return `Помилка: ${state.error}`;
}
}
console.log(
describeState({
status: "success",
data: {
id: 1,
name: "Олена",
},
}),
);Перевірка state.status звужує тип state. У гілці "success" TypeScript знає, що властивість data існує.
Type alias можна створити за допомогою оператора &:
interface PageInfo {
page: number;
pageSize: number;
total: number;
}
type PaginatedResponse<T> = ApiResponse<T[]> & PageInfo;
const usersPage: PaginatedResponse<User> = {
data: [
{
id: 1,
name: "Олена",
},
],
status: 200,
page: 1,
pageSize: 10,
total: 1,
};PaginatedResponse<User> поєднує властивості:
ApiResponse<User[]>;
PageInfo.
Отже, data має тип User[], а об’єкт також повинен містити поля пагінації.
Для простих об’єктних структур обидва підходи часто дають однаковий результат:
interface UserInterface {
id: number;
name: string;
}
type UserAlias = {
id: number;
name: string;
};Обидва типи можна використовувати для перевірки об’єктів:
const userFromInterface: UserInterface = {
id: 1,
name: "Олена",
};
const userFromAlias: UserAlias = {
id: 2,
name: "Андрій",
};Вибір залежить від структури, яку потрібно описати.
Інтерфейс добре підходить, коли описується форма об’єкта або контракт класу:
interface Logger {
log(message: string): void;
error(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(message);
}
error(message: string): void {
console.error(message);
}
}Інтерфейси підтримують успадкування через extends:
interface BaseEntity {
id: number;
createdAt: Date;
}
interface UserEntity extends BaseEntity {
name: string;
}UserEntity містить усі властивості BaseEntity і додатково має name.
Інтерфейс також можна розширити окремим оголошенням з тією самою назвою:
interface Configuration {
environment: "development" | "production";
}
interface Configuration {
version: string;
}
const configuration: Configuration = {
environment: "development",
version: "1.0.0",
};Таку можливість називають declaration merging. Вона може бути корисною для розширення типів, але в коді застосунку випадкове дублювання назв може ускладнювати підтримку.
Type alias підходить, коли потрібно описати:
об’єднання типів через |;
перетин типів через &;
кортежі;
примітивні типи з власною назвою;
типи функцій;
складні комбінації інших типів.
type Identifier = string | number;
type Coordinates = [number, number];
type Formatter<T> = (value: T) => string;
const formatUser: Formatter<User> = (user) => {
return `${user.id}: ${user.name}`;
};Type alias може бути узагальненим і рекурсивним:
type TreeNode<T> = {
value: T;
children: TreeNode<T>[];
};
const numberTree: TreeNode<number> = {
value: 1,
children: [
{
value: 2,
children: [],
},
],
};Узагальнений інтерфейс можна розширити, підставивши конкретний тип:
interface Response<T> {
data: T;
status: number;
}
interface UserResponse extends Response<User> {
permissions: string[];
}
const response: UserResponse = {
data: {
id: 1,
name: "Олена",
},
status: 200,
permissions: ["read", "write"],
};Для type alias аналогічна структура створюється через перетин:
type UserResponseAlias = Response<User> & {
permissions: string[];
};Обидва варіанти описують об’єкт із даними користувача, HTTP-статусом і дозволами. Інтерфейс зазвичай читатиметься природніше, коли це іменований контракт об’єкта, а type alias — коли структура комбінується з іншими типами.
Іноді generic не повинен приймати будь-який тип. Для цього використовують extends:
interface HasId {
id: number;
}
function getId<T extends HasId>(item: T): number {
return item.id;
}
const userId = getId({
id: 10,
name: "Марія",
});
console.log(userId);T extends HasId означає, що переданий тип повинен мати властивість id типу number.
Неправильний виклик TypeScript відхилить:
// getId({ name: "Марія" }); // Помилка: властивість id відсутняОбмеження не забороняє додаткові властивості. Тому об’єкт користувача підходить, оскільки має id і name.
Для параметра типу можна вказати значення за замовчуванням:
type Result<T, E = string> =
| {
ok: true;
value: T;
}
| {
ok: false;
error: E;
};Якщо другий параметр не передано, E буде string:
const success: Result<number> = {
ok: true,
value: 42,
};
const failure: Result<number> = {
ok: false,
error: "Не вдалося завантажити дані",
};За потреби тип помилки можна змінити:
type ValidationError = {
field: string;
message: string;
};
const validationFailure: Result<User, ValidationError[]> = {
ok: false,
error: [
{
field: "name",
message: "Ім’я є обов’язковим",
},
],
};У цьому прикладі узагальнений інтерфейс описує відповідь API, а type alias — можливі результати операції:
interface ApiResponse<T> {
data: T;
status: number;
}
type OperationResult<T, E = string> =
| {
ok: true;
value: T;
}
| {
ok: false;
error: E;
};
type User = {
id: number;
name: string;
};
function parseUser(
response: ApiResponse<unknown>,
): OperationResult<User> {
if (
typeof response.data === "object" &&
response.data !== null &&
"id" in response.data &&
"name" in response.data &&
typeof response.data.id === "number" &&
typeof response.data.name === "string"
) {
return {
ok: true,
value: {
id: response.data.id,
name: response.data.name,
},
};
}
return {
ok: false,
error: "Некоректні дані користувача",
};
}
const apiResponse: ApiResponse<unknown> = {
data: {
id: 1,
name: "Олена",
},
status: 200,
};
const result = parseUser(apiResponse);
if (result.ok) {
console.log(`Користувач: ${result.value.name}`);
} else {
console.error(`Помилка: ${result.error}`);
}Тут:
ApiResponse<T> описує відповідь із даними довільного типу;
OperationResult<T, E> описує успішний або невдалий результат;
E = string задає типовий тип помилки;
перевірка result.ok звужує об’єднання до відповідної гілки.
Якщо generic не має значення за замовчуванням, під час використання потрібно вказати параметр типу:
interface Box<T> {
value: T;
}
// const box: Box = { value: 10 }; // Помилка
const box: Box<number> = {
value: 10,
};anyany вимикає значну частину перевірок TypeScript:
interface UnsafeResponse {
data: any;
}Краще використовувати generic, коли тип даних відомий:
interface SafeResponse<T> {
data: T;
}А якщо тип справді невідомий на момент отримання даних, доречніше почати з unknown і виконати перевірку перед використанням.
| та &Об’єднання | означає «один із типів»:
type Id = string | number;Перетин & означає «усі типи одночасно»:
type AuditedUser = User & {
lastLogin: Date;
};Об’єкт типу AuditedUser повинен мати властивості і User, і додаткову lastLogin.
Обмеження має описувати мінімальний набір властивостей, який потрібен функції:
function printId<T extends { id: number }>(item: T): void {
console.log(item.id);
}Не потрібно обмежувати T повним типом User, якщо функції потрібне лише поле id.
Generic дають змогу повторно використовувати структури з різними типами даних.
Узагальнений інтерфейс зручно застосовувати для контрактів об’єктів і класів.
Узагальнений type alias зручний для об’єднань, перетинів, функцій, кортежів і складених типів.
Інтерфейси можна розширювати через extends і declaration merging.
Type aliases комбінуються через | та &.
extends обмежує допустимі типи параметра generic.
Значення за замовчуванням роблять параметри типу необов’язковими.
Вибір між interface і type залежить від форми структури та способу її подальшого використання.