Пошук уроків, статей та іншого контенту
Пояснюємо, навіщо потрібні узагальнені типи, як вони зберігають типобезпеку та чим відрізняються від типу any.
Generics, або узагальнені типи, дають змогу писати код, який працює з різними типами, але не втрачає інформацію про конкретний тип.
Наприклад, функція може повертати передане значення:
function identity(value: string): string {
return value;
}Така функція працює лише з рядками. Якщо створити окрему функцію для кожного типу, код буде дублюватися:
function identityString(value: string): string {
return value;
}
function identityNumber(value: number): number {
return value;
}Generic дає змогу описати цю залежність один раз:
function identity<T>(value: T): T {
return value;
}Тут T — це параметр типу. Він означає:
Функція приймає значення типу
Tі повертає значення того самого типуT.
T не є конкретним типом. Під час виклику TypeScript визначає, яким типом він має бути.
const text = identity("TypeScript");
// text має тип string
const count = identity(42);
// count має тип number
const isActive = identity(true);
// isActive має тип booleanТип T існує лише під час перевірки TypeScript. У готовому JavaScript узагальнені типи не зберігаються.
Найчастіше тип визначається автоматично на основі аргументу:
function wrap<T>(value: T): T[] {
return [value];
}
const numbers = wrap(10);
// number[]
const names = wrap("Anna");
// string[]У цьому прикладі:
для wrap(10) T стає number;
для wrap("Anna") T стає string;
результат завжди є масивом того самого типу.
За потреби тип можна вказати явно:
const numbers = wrap<number>(10);
const names = wrap<string>("Anna");Зазвичай автоматичного визначення достатньо, тому явний запис використовують лише тоді, коли він робить код зрозумілішим або допомагає уникнути неоднозначності.
Generic особливо корисний для функцій, які працюють із масивами.
function getFirst<T>(items: T[]): T | undefined {
return items[0];
}
const firstNumber = getFirst([10, 20, 30]);
// number | undefined
const firstName = getFirst(["Anna", "Oleh"]);
// string | undefinedФункція getFirst не знає наперед, з яким типом працюватиме. Але TypeScript зберігає цю інформацію:
масив чисел дає результат number | undefined;
масив рядків дає результат string | undefined.
undefined потрібен, тому що масив може бути порожнім.
const first = getFirst([]);
if (first !== undefined) {
console.log(first);
}anyТип any вимикає більшість перевірок TypeScript для конкретного значення.
let value: any = "hello";
value.toUpperCase(); // дозволено
value.toFixed(2); // також дозволено під час перевіркиДругий виклик є помилковим під час виконання, оскільки метод toFixed належить числам, а не рядкам. TypeScript не попередить про це, бо значення має тип any.
Generic, навпаки, зберігає справжній тип:
function identity<T>(value: T): T {
return value;
}
const value = identity("hello");
value.toUpperCase(); // правильно
value.toFixed(2); // помилка TypeScriptПісля виклику identity("hello") TypeScript знає, що value має тип string. Тому він не дозволяє викликати метод, якого в рядка немає.
| Generics | any | |---|---| | Зберігають інформацію про тип | Приховують інформацію про тип | | Перевіряються TypeScript | Переважно обходять перевірки | | Дозволяють працювати з різними типами без дублювання | Дозволяють майже будь-які операції | | Допомагають виявляти помилки до запуску | Можуть перенести помилки на час виконання |
Узагальнені типи можна використовувати не лише у функціях, а й в інтерфейсах.
interface ApiResponse<T> {
data: T;
success: boolean;
}
const userResponse: ApiResponse<{ name: string; age: number }> = {
data: {
name: "Anna",
age: 25,
},
success: true,
};
const messageResponse: ApiResponse<string> = {
data: "Дані отримано",
success: true,
};
console.log(userResponse.data.name);
console.log(messageResponse.data.toUpperCase());ApiResponse<T> описує загальну структуру відповіді:
success завжди має тип boolean;
data може мати різні типи.
Для userResponse T — це об’єкт користувача, а для messageResponse T — це string.
Іноді функція повинна працювати не з будь-яким типом, а лише з типами, які мають певну властивість.
Наприклад, щоб отримати довжину значення, потрібна властивість length:
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
console.log(getLength("TypeScript"));
console.log(getLength([1, 2, 3]));Запис T extends { length: number } означає:
Тип
Tповинен мати властивістьlengthчислового типу.
Тому рядок і масив підходять, а число — ні:
getLength(42); // помилка TypeScriptОбмеження не перетворює все на any. Навпаки, воно повідомляє TypeScript, які властивості безпечно використовувати всередині функції.
function getFirst<T>(items: T[]): T | undefined {
return items[0];
}
interface Box<T> {
value: T;
}
function createBox<T>(value: T): Box<T> {
return { value };
}
const numbers = [10, 20, 30];
const firstNumber = getFirst(numbers);
if (firstNumber !== undefined) {
console.log(firstNumber.toFixed(2));
}
const textBox = createBox("hello");
console.log(textBox.value.toUpperCase());
const userBox = createBox({
name: "Anna",
age: 25,
});
console.log(userBox.value.name);
console.log(userBox.value.age);У цьому прикладі TypeScript автоматично виводить:
firstNumber як number | undefined;
textBox як Box<string>;
userBox як Box<{ name: string; age: number }>.
Жоден із цих типів не потрібно дублювати вручну.
any замість genericfunction getValue(value: any): any {
return value;
}Така функція приймає будь-яке значення, але результат втрачає корисну типову інформацію.
Краще:
function getValue<T>(value: T): T {
return value;
}undefinedФункція, яка бере елемент масиву, може не знайти його:
function getFirst<T>(items: T[]): T | undefined {
return items[0];
}Не варто вказувати лише T, якщо масив може бути порожнім. Результат справді може бути undefined.
Якщо функція потребує лише властивості length, не потрібно обмежувати її лише рядками:
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}Так функція працюватиме і з рядками, і з масивами, зберігаючи перевірку типів.
Generics дають змогу писати повторно використовуваний код для різних типів.
Параметр типу, наприклад T, описує зв’язок між вхідними та вихідними значеннями.
TypeScript часто визначає generic-тип автоматично.
Generics зберігають типобезпеку, а any переважно вимикає перевірки.
Узагальнені типи можна використовувати у функціях, масивах, інтерфейсах і власних типах.
За допомогою extends можна обмежити типи, з якими працює generic.