Багато разів у TypeScript-коді ми стикаємося з необхідністю створення компонентів або функцій, які працюють з різними типами даних. Без generics код стає занадто шаблонним, громіздким та схильним до помилок. Наприклад, уявимо, що потрібно написати функцію для об’єднання двох масивів, але тип елементів у цих масивах може бути різним.
Це призводить до повторення коду та зниження його читабельності. Без generics, для кожної нової комбінації типів доведеться писати окрему функцію, що значно збільшує обсяг коду та час на його підтримку.
Контекст і чому це важливо
Generics дозволяють писати більш узагальнений код, який працює з різними типами даних, не втрачаючи при цьому типів безпеки. Вони особливо корисні при роботі з колекціями даних, функціями обробки та компонентами UI. Наприклад, кастомні хуки в React або обробники подій.
Ігнорування generics призводить до надмірної дублювання коду, підвищує ймовірність помилок, та ускладнює рефакторинг. Команда розробників витрачає більше часу на підтримку коду, а не на розробку нових функцій. Це може призвести до зниження продуктивності команди на 10-15%.
Практична реалізація
Generics дозволяють параметризувати типи, використовуючи кутові дужки після імені функції або класу. Це дозволяє створити компонент, який працює з будь-яким типом даних, визначаючи його лише під час використання.
function identity<T>(arg: T): T {
// Функція, що повертає аргумент без змін
return arg;
}
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
// Приклад використання
const numberIdentity = identity<number>(5); // numberIdentity: 5
const stringIdentity = identity<string>("hello"); // stringIdentity: "hello"
const apiResponse: ApiResponse<{ name: string; age: number }> = {
data: { name: "John", age: 30 },
status: 200,
message: "Success"
};
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2
Цей код демонструє створення функції `identity` та класу `Stack`, які можуть працювати з будь-яким типом даних, визначеним параметром типу `T`. Це дозволяє використовувати ці компоненти в різних сценаріях, не переписуючи їх.
Поширені помилки та підводні камені
- Неправильне вказування типу: Якщо не вказати тип `T` при виклику функції з generics, TypeScript може спробувати його вивести, але це не завжди спрацьовує коректно. Це може призвести до непередбачених помилок під час виконання.
- Використання `any` всередині generics: Це нівелює всі переваги типів безпеки, що надає TypeScript. Завжди намагайтеся вказувати конкретні типи.
- Забування про обмеження (constraints): Generics можна обмежити певними типами, наприклад, щоб вони були лише класами з певним методом. Нехтування цим може призвести до помилок під час компіляції.
Порівняння підходів
Без generics, для кожної нової комбінації типів доводиться писати окрему функцію. Наприклад, якщо потрібно об’єднати масиви чисел та рядків, доведеться писати окрему функцію для кожного випадку. Це призводить до дублювання коду і ускладнює його підтримку.
Використання generics дозволяє створити одну функцію, яка працює з будь-якими типами даних. Це скорочує обсяг коду на 30-50% і значно полегшує його підтримку. Крім того, це підвищує читабельність коду та знижує ймовірність помилок.
Висновки
Generics — потужний інструмент для написання гнучкого та безпечного коду на TypeScript. Використовуйте їх, коли потрібно створити компоненти, які працюють з різними типами даних. Почніть з простого прикладу, наприклад, з функції `identity`, та поступово освоюйте більш складні сценарії.