У розробці масштабованих застосунків ключову роль відіграє повторне використання коду без втрати типобезпеки. Без узагальненого програмування розробники змушені або дублювати ідентичні структури для кожного типу даних, або вдаватися до типу any, що повністю руйнує статичну верифікацію.
Узагальнення (Generics) — це параметри для типів. Так само, як звичайні функції приймають аргументи-значення для обчислення результату в рантаймі, конструктори типів приймають параметри типу (<T>) для обчислення результуючого статичного типу на етапі компіляції.
На базі узагальнень стандартна бібліотека TypeScript надає сімейство вбудованих утилітних типів (Utility Types): Partial<T>, Required<T>, Readonly<T>, Pick<T, K>, Omit<T, K>, Record<K, T>, ReturnType<T> та інших. Ці утиліти дозволяють трансформувати існуючі моделі даних без ручного дублювання інтерфейсів.
Погляньмо на відповідність між світом значень і світом типів:
+-------------------------------------------------------------------------+
| АНАЛОГІЯ: ЗНАЧЕННЯ vs ТИПИ |
+-------------------------------------------------------------------------+
| СВІТ ЗНАЧЕНЬ (Рантайм): |
| function wrapValue(val) { |
| return { data: val }; |
| } |
| wrapValue(42) ---> { data: 42 } |
+-------------------------------------------------------------------------+
| СВІТ ТИПІВ (Компіляція): |
| type ApiResponse<T> = { |
| data: T; |
| status: number; |
| }; |
| ApiResponse<User> ---> { data: User; status: number } |
| ApiResponse<Post> ---> { data: Post; status: number } |
+-------------------------------------------------------------------------+
| СТАНДАРТНІ ТРАНСФОРМАЦІЇ (Utility Types): |
| Partial<User> ---> Усі поля стають опціональними (?) |
| Pick<User, 'id'> ---> Залишає ЛИШЕ вказані ключі |
| Omit<User, 'pwd'> ---> Виключає вказані ключі з типу |
+-------------------------------------------------------------------------+<T>)
Розробник викликає функцію `identity(42)` без явного зазначення `identity<number>(42)`.
Компілятор досліджує позицію аргументу та пов'язує значення `42` з типовою змінною `T`.
Якщо оголошено `T extends Entity`, компілятор звіряє виведений тип з базовим обмеженням.
Тип `T` фіксується, і всі залежні позиції (тип, що повертається, інші аргументи) отримують точний тип.
Яку роль виконує ключове слово `extends` в оголошенні `function getEntityId<T extends { id: string }>(entity: T): string`?
apiClient.get<UserProfile>('/me'), що гарантують правильний тип даних відповіді.const [user, setUser] = useState<User | null>(null).Partial<User> для ендпоінтів PATCH-запитів.Record<string, CacheEntry> або Record<Locale, Dictionary>.Компілятор автоматично виводить тип T на основі переданого аргументу:
// Узагальнена функція-обгортка
function wrapResult<T>(value: T): { success: true; payload: T } {
return { success: true, payload: value };
}
const numResult = wrapResult(100); // Тип: { success: true; payload: number }
const strResult = wrapResult('OK'); // Тип: { success: true; payload: string }
// Узагальнений інтерфейс контейнера
interface PaginatedList<TItem> {
items: TItem[];
total: number;
page: number;
hasMore: boolean;
}
interface Article {
slug: string;
title: string;
}
type ArticleFeed = PaginatedList<Article>;Partial, Pick, Omit, Record#interface UserProfile {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
// 1. Partial<T>: робить усі поля опціональними (для PATCH-запитів)
type UpdateUserDto = Partial<UserProfile>;
// Еквівалентно: { id?: string; name?: string; email?: string; role?: 'admin' | 'user'; createdAt?: Date }
// 2. Pick<T, K>: вибирає лише вказані поля
type UserPreview = Pick<UserProfile, 'id' | 'name'>;
// Еквівалентно: { id: string; name: string }
// 3. Omit<T, K>: виключає вказані поля (наприклад, прибираємо id та createdAt при створенні)
type CreateUserDto = Omit<UserProfile, 'id' | 'createdAt'>;
// Еквівалентно: { name: string; email: string; role: 'admin' | 'user' }
// 4. Record<K, T>: створює типізований словник ключ-значення
type UserRegistry = Record<string, UserProfile>;
type RolePermissions = Record<'admin' | 'user', string[]>;
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read'],
};keyof#Зв'язування двох параметрів типу дозволяє створювати абсолютно безпечні хелпери доступу до властивостей:
// K зобов'язаний бути ключем об'єкта T!
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const profile: UserProfile = {
id: 'usr_1',
name: 'Anna',
email: '[email protected]',
role: 'admin',
createdAt: new Date(),
};
const userEmail = getProperty(profile, 'email'); // Тип userEmail строго string!
const creationDate = getProperty(profile, 'createdAt'); // Тип строго Date!
// ПОМИЛКА КОМПІЛЯЦІЇ:
// getProperty(profile, 'nonExistentProperty');
// TS2345: Argument of type '"nonExistentProperty"' is not assignable to parameter of type 'keyof UserProfile'.Який тип вийде в результаті застосування `Omit<UserProfile, 'id' | 'role'>`, якщо `UserProfile` містить поля `id`, `name`, `email`, `role`, `createdAt`?
Не створюйте узагальнення заради узагальнень:
// НАДМІРНО І ПОГАНО:
function printId<T extends string>(id: T): void {
console.log(id);
}
// ПРАВИЛЬНО І ЛАКОНІЧНО:
function printId(id: string): void {
console.log(id);
}Узагальнення необхідне лише тоді, коли між типами кількох аргументів або типом значення, що повертається, існує логічний зв'язок.
Небезпечний патерн, що вимикає перевірки типів.
Рішення: Використовуйте Record<string, unknown> для невідомих значень з подальшою валідацією, або строгі типи.