Generics
TypeScript Generics — ряд однакових скляних банок на полиці кожна з різним вмістом та підписом
Уяви, що ти пишеш функцію, яка повертає перший елемент масиву. Вона має працювати з масивом чисел, рядків, об'єктів -- будь-чого. Як типізувати таку функцію? Використати any? Ні, бо втратиш type safety. Використати overloading для кожного типу? Занадто багато коду. Рішення -- Generics.
Навіщо потрібні Generics?
Проблема без generics
// Варіант 1: any (погано -- втрачаємо типізацію)
function getFirst(arr: any[]): any {
return arr[0];
}
const first = getFirst([1, 2, 3]); // first: any -- TypeScript не знає тип!
// Варіант 2: overloading (занадто багато коду)
function getFirst(arr: number[]): number;
function getFirst(arr: string[]): string;
function getFirst(arr: any[]): any {
return arr[0];
}
// А якщо потрібен масив boolean? Об'єктів? Знову додавати overload?
Рішення -- Generic
// T -- "заповнювач типу", який буде визначений при виклику
function getFirst<T>(arr: T[]): T {
return arr[0];
}
const num = getFirst([1, 2, 3]); // num: number
const str = getFirst(["hello", "world"]); // str: string
const bool = getFirst([true, false]); // bool: boolean
// TypeScript визначає T автоматично з аргументів!
<T> -- це параметр типу (type parameter). Він працює як параметр функції, але для типів.
Generic Functions
Синтаксис
// function declaration
function identity<T>(value: T): T {
return value;
}
// arrow function
const identity2 = <T>(value: T): T => value;
// Виклик -- TypeScript виводить T автоматично
identity(42); // T = number
identity("hello"); // T = string
// Або можна вказати явно
identity<string>("hello"); // T = string явно
Кілька параметрів типу
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const result = pair("Олексій", 25); // [string, number]
// Функція map
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
const numbers = [1, 2, 3];
const strings = mapArray(numbers, (n) => n.toString());
// strings: string[]
const lengths = mapArray(["hi", "hello"], (s) => s.length);
// lengths: number[]
Generic Interfaces
// Generic interface для API response
interface ApiResponse<T> {
data: T;
success: boolean;
timestamp: string;
}
// Конкретні типи при використанні
interface User {
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
const userResponse: ApiResponse<User> = {
data: { name: "Олексій", email: "alex@test.com" },
success: true,
timestamp: "2026-02-09T10:00:00",
};
const productsResponse: ApiResponse<Product[]> = {
data: [
{ id: 1, title: "Ноутбук", price: 25000 },
{ id: 2, title: "Телефон", price: 15000 },
],
success: true,
timestamp: "2026-02-09T10:00:00",
};
Generic interface для колекції
interface Collection<T> {
items: T[];
add(item: T): void;
remove(index: number): T | undefined;
find(predicate: (item: T) => boolean): T | undefined;
getAll(): T[];
}
// Реалізація
function createCollection<T>(initial: T[] = []): Collection<T> {
const items: T[] = [...initial];
return {
items,
add(item) {
items.push(item);
},
remove(index) {
return items.splice(index, 1)[0];
},
find(predicate) {
return items.find(predicate);
},
getAll() {
return [...items];
},
};
}
// Використання
const users = createCollection<User>([
{ name: "Олексій", email: "alex@test.com" },
]);
users.add({ name: "Марія", email: "maria@test.com" });
const found = users.find((u) => u.name === "Марія");
// found: User | undefined
Generic Type Aliases
// Nullable тип
type Nullable<T> = T | null;
let userName: Nullable<string> = "Олексій";
userName = null; // OK
// Result тип (успіх або помилка)
type Result<T, E = string> =
| { success: true; data: T }
| { success: false; error: E };
function divide(a: number, b: number): Result<number> {
if (b === 0) {
return { success: false, error: "Ділення на нуль" };
}
return { success: true, data: a / b };
}
const result = divide(10, 3);
if (result.success) {
console.log(result.data); // number -- TypeScript знає!
} else {
console.log(result.error); // string
}
Constraints -- обмеження generics
Іноді потрібно обмежити, які типи можна передати в generic. Для цього використовується extends:
// T повинен мати властивість length
function logLength<T extends { length: number }>(value: T): void {
console.log(`Довжина: ${value.length}`);
}
logLength("hello"); // OK: string має length
logLength([1, 2, 3]); // OK: array має length
logLength({ length: 5 }); // OK: об'єкт має length
// logLength(42); // Помилка: number не має length
Constraint з keyof
// K повинен бути ключем об'єкта T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Олексій", age: 25, email: "alex@test.com" };
getProperty(user, "name"); // string
getProperty(user, "age"); // number
// getProperty(user, "phone"); // Помилка: "phone" не є ключем user
keyof T повертає union type всіх ключів об'єкта T. У прикладі вище keyof typeof user -- це "name" | "age" | "email".
Constraint extends interface
interface HasId {
id: number;
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id);
}
const products = [
{ id: 1, title: "Ноутбук", price: 25000 },
{ id: 2, title: "Телефон", price: 15000 },
];
const found = findById(products, 1);
// found: { id: number; title: string; price: number } | undefined
Default Type Parameters
Generics можуть мати значення за замовчуванням:
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
// Можна не вказувати T
const response: ApiResponse = {
data: "something",
status: 200,
};
// Або вказати явно
const userResponse: ApiResponse<User> = {
data: { name: "Олексій", email: "alex@test.com" },
status: 200,
};
Utility Types -- вбудовані generics
TypeScript має набір вбудованих generic типів, які вирішують типові задачі:
Partial<T> -- всі поля стають optional
interface User {
name: string;
email: string;
age: number;
}
// Partial<User> = { name?: string; email?: string; age?: number }
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}
const user: User = { name: "Олексій", email: "alex@test.com", age: 25 };
const updated = updateUser(user, { age: 26 }); // Тільки age
Required<T> -- всі поля стають обов'язковими
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
// Required<Config> = { host: string; port: number; debug: boolean }
const fullConfig: Required<Config> = {
host: "localhost",
port: 3000,
debug: true,
};
Pick<T, K> -- обрати тільки вказані поля
interface User {
name: string;
email: string;
age: number;
password: string;
}
// Тільки name та email
type PublicUser = Pick<User, "name" | "email">;
// { name: string; email: string }
const publicUser: PublicUser = {
name: "Олексій",
email: "alex@test.com",
};
Omit<T, K> -- виключити вказані поля
// Все крім password
type SafeUser = Omit<User, "password">;
// { name: string; email: string; age: number }
// Omit -- протилежність Pick
Record<K, V> -- об'єкт з ключами K та значеннями V
// Словник переклади
type Language = "uk" | "en" | "de";
const greetings: Record<Language, string> = {
uk: "Привіт",
en: "Hello",
de: "Hallo",
};
// Record для scores
type Subject = "math" | "english" | "physics";
const scores: Record<Subject, number> = {
math: 95,
english: 87,
physics: 92,
};
Readonly<T> -- всі поля стають readonly
interface User {
name: string;
email: string;
}
const frozenUser: Readonly<User> = {
name: "Олексій",
email: "alex@test.com",
};
// frozenUser.name = "Марія"; // Помилка: Cannot assign to 'name'
Ці utility types можна комбінувати: Partial<Pick<User, "name" | "email">> -- optional name та email. Це дуже зручно для CRUD-операцій: Create потребує Omit<User, "id">, Update -- Partial<Omit<User, "id">>.
Практика: generic storage
interface StorageService<T extends { id: string | number }> {
getAll(): T[];
getById(id: T["id"]): T | undefined;
add(item: T): void;
update(id: T["id"], updates: Partial<Omit<T, "id">>): T | undefined;
remove(id: T["id"]): boolean;
}
function createStorage<T extends { id: string | number }>(
initial: T[] = []
): StorageService<T> {
const items: T[] = [...initial];
return {
getAll: () => [...items],
getById: (id) => items.find((item) => item.id === id),
add: (item) => {
items.push(item);
},
update: (id, updates) => {
const index = items.findIndex((item) => item.id === id);
if (index === -1) return undefined;
items[index] = { ...items[index], ...updates };
return items[index];
},
remove: (id) => {
const index = items.findIndex((item) => item.id === id);
if (index === -1) return false;
items.splice(index, 1);
return true;
},
};
}
// Використання з різними типами
interface Product {
id: number;
title: string;
price: number;
}
const productStorage = createStorage<Product>([
{ id: 1, title: "Ноутбук", price: 25000 },
]);
productStorage.add({ id: 2, title: "Телефон", price: 15000 });
productStorage.update(1, { price: 23000 });
const product = productStorage.getById(1);
// product: Product | undefined
Підсумок
- Generics (
<T>) -- параметри типів, які визначаються при використанні - Generic functions:
function fn<T>(arg: T): T - Generic interfaces:
interface Box<T> { value: T } - Generic type aliases:
type Result<T> = { data: T } - Constraints (
extends) -- обмежують допустимі типи для generic - keyof -- отримує union ключів об'єкта
- Default types:
<T = string>-- значення за замовчуванням - Utility types:
Partial,Required,Pick,Omit,Record,Readonly
Що далі?
В останньому уроці блоку ми навчимось використовувати TypeScript з DOM -- типізувати HTML-елементи, писати type guards, робити type assertions та типізувати fetch-запити. Це підготує тебе до роботи з React у Block 8.
Корисні посилання:
- TypeScript Handbook: Generics -- офіційна документація
- TypeScript Handbook: Utility Types -- всі вбудовані utility types
- Total TypeScript: Generics Tutorial -- інтерактивний курс
- TypeScript Playground -- експериментуй з generics