Фото від Emile Perron на Unsplash
TypeScript генерики (generics) — це потужна можливість, яка дозволяє розробникам писати багаторазові, типобезпечні компоненти та функції, зберігаючи при цьому гнучкість. У цій статті ми розглянемо основи генериків, з чіткими поясненнями та практичними прикладами. Ви дізнаєтесь, як використовувати генерики та повністю розкрити їх потенціал.
Що таке генерики?
Генерики дозволяють створювати компоненти, функції та класи, які працюють з різними типами, забезпечуючи типобезпечність. Замість того, щоб визначати конкретні типи, генерики використовують параметри типів, такі як T
(скорочено від Type — тип), U
(скорочено від Union — об'єднання) або K
(скорочено від Key — ключ), які можуть бути замінені конкретними типами під час виконання. Ви можете уявити тип генерика як заповнювач, який можна замінити конкретним типом, коли код виконується або ініціалізується.
Приклад генеричної функції
function genericFuncion(value: T): T {
return value;
}
// Використання функції з різними типами
const genericNumber = genericFuncion(42); // T це number
console.log(genericNumber); // Виведено: 42
const genericString = genericFuncion("Hello, Generics!"); // T це string
console.log(genericString); // Виведено: Hello, Generics!
const genericArray = genericFuncion([1, 2, 3]); // T це number[]
console.log(genericArray); // Виведено: [1, 2, 3]
Тут функція genericFunction
приймає параметр типу T
, що дозволяє їй обробляти будь-який тип, наданий викликом. Коли функція genericFunction
використовується, T
замінюється на фактичний тип переданого аргументу.
Навіщо використовувати генерики?
- Повторне використання: Напишіть код один раз і використовуйте його з різними типами.
- Типобезпечність: Забезпечте узгодженість типів, не вдаючись до
any
.
Catch type errors during development, reducing runtime issues. - Гнучкість (Flexibility): Створюйте компоненти та бібліотеки, які працюють без проблем з різними структурами даних, і обробляють динамічні типи, зберігаючи суворі обмеження.
- Читабельність (Readability): Чітко передавайте намір типових зв'язків.
Генеричні функції
Базовий синтаксис
Генерична функція приймає один або кілька параметрів типів, огорнутих в кутові дужки (<>
).
function genericFunction(value: T): T {
return value;
}
Використання кількох параметрів типів
Ви можете визначити функції з кількома параметрами типів, щоб працювати з більш складними випадками.
function genericFunction(first: T, second: U): [T, U] {
return [first, second];
}
console.log(genericFunction("Age", 30)); // Виведено: ["Age", 30]
Генеричні інтерфейси
Генерики можна також застосовувати до інтерфейсів, роблячи їх адаптованими до різних типів.
Приклад: Генеричний інтерфейс
interface KeyValuePair {
key: K;
value: V;
}
const pair: KeyValuePair = {
key: "Age",
value: 30,
};
console.log(pair); // Виведено: { key: "Age", value: 30 }
interface List {
items: T[];
addItem(item: T): void;
getItem(index: number): T;
}
class StringList implements List {
items: string[] = [];
addItem(item: string): void {
this.items.push(item);
}
getItem(index: number): string {
return this.items[index];
}
}
const myList = new StringList();
myList.addItem("TypeScript");
console.log(myList.getItem(0)); // Виведено: TypeScript
Генеричні класи
Класи також можуть використовувати генерики, що дозволяє створювати багаторазові та типобезпечні структури класів.
Приклад: Генеричний клас
class Stack {
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];
}
}
const stringStack = new Stack();
stringStack.push("Hello");
console.log(stringStack.peek()); // Виведено: Hello
const numberStack = new Stack();
numberStack.push(42);
console.log(numberStack.pop()); // Виведено: 42
Обмеження для генериків
Іноді ви можете захотіти обмежити типи, які може приймати параметр типу. Це можна зробити за допомогою обмежень, використовуючи ключове слово extends
.
Приклад: Обмеження генеричного типу
function getLength(input: T): number {
return input.length;
}
// Використання
console.log(getLength("Hello")); // Працює: String має властивість length
console.log(getLength([1, 2, 3])); // Працює: Масив має властивість length
// console.log(getLength(42)); // Помилка: Число не має властивості length
Тут T
обмежено типами, що мають властивість length
.
Генеричні утилітні типи
TypeScript надає кілька вбудованих утилітних типів, що використовують генерики, таких як Partial
, Readonly
, Record
і Pick
.
Ці утиліти спрощують звичайні перетворення типів.
Partial
: Робить усі властивості вT
необов'язковими.Required
: Робить усі властивості вT
обов'язковими.Readonly
: Робить усі властивості вT
лише для читання.Record
: Створює тип об'єкта з ключамиK
і значеннямиT
.
Приклад: Використання Partial
і Readonly
interface User {
id: number;
name: string;
age?: number;
}
const updateUser = (user: Partial<User>): void => {
console.log(user);
};
updateUser({ name: "John" }); // Виведено: { name: "John" }
const readonlyUser: Readonly<User> = {
id: 1,
name: "Alice",
};
// readonlyUser.name = "Bob"; // Помилка: Неможливо присвоїти 'name', оскільки це властивість тільки для читання.
Просунути генерики
Значення за замовчуванням для параметрів типу
Ви можете задати значення за замовчуванням для параметрів типів, щоб зробити ваші функції чи класи більш гнучкими.
function createArray(length: number, value: T): T[] {
return Array(length).fill(value);
}
console.log(createArray(3, "Hello")); // Виведено: ["Hello", "Hello", "Hello"]
console.log(createArray(3, 42)); // Виведено: [42, 42, 42]
Умовні типи
Генерики можна поєднувати з умовними типами для складної логіки типів.
type IsArray<T> = T extends any[] ? "array" : "not an array";
type Test1 = IsArray<string[]>; // "array"
type Test2 = IsArray<number>; // "not an array"
Генерики в реальних сценаріях
Генеричний обгортка для відповіді API
Генерики особливо корисні в сценаріях, де структури даних динамічні, наприклад, при роботі з відповідями від API.
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
function fetchData<T>(url: string): Promise<ApiResponse<T>> {
return fetch(url)
.then((response) => response.json())
.then((data) => ({
data,
status: response.status,
message: response.statusText,
}));
}
interface User {
id: number;
name: string;
}
fetchData<User>('/api/users').then((response) => {
console.log(response.data); // Масив об'єктів User
});
Генеричні утилітні функції
function mapArray<T, U>(array: T[], callback: (item: T) => U): U[] {
return array.map(callback);
}
const numbers = [1, 2, 3];
const strings = mapArray(numbers, (num) => `Number: ${num}`);
console.log(strings); // Виведено: ["Number: 1", "Number: 2", "Number: 3"]
Кращі практики при використанні генериків
- Інтуїтивно називайте параметри типів: Використовуйте значущі імена, такі як
TKey
таTValue
, коли це важливо для ясності. - Використовуйте обмеження: Додавайте обмеження, щоб уникнути помилок і зробити ваш код передбачуваним.
- Уникайте надмірного використання генериків: Не використовуйте генерики там, де існують простіші рішення. Наприклад, не використовуйте
Array
замістьstring[]
абоnumber[]
. - Документуйте складну логіку генериків: Коли працюєте з просунутими генериками, додавайте коментарі для покращення підтримки.
Висновок
Генерики TypeScript є основою його системи типів, дозволяючи писати код, який є одночасно багаторазовим і типобезпечним. Від простих функцій до складних утиліт типів, генерики підвищують гнучкість без компромісів у читабельності та підтримуваності. Завдяки розумінню та ефективному використанню генериків ви зможете писати більш надійний, масштабований і підтримуваний код на TypeScript. Незалежно від того, чи створюєте ви бібліотеки, працюєте зі структурами даних чи створюєте утилітні типи, генерики є потужним інструментом у вашому арсеналі TypeScript.
Почніть використовувати генерики у вашій кодовій базі TypeScript, і ви побачите значне покращення якості та підтримуваності коду.
Перекладено з: A Comprehensive Guide to TypeScript Generics