Пошук уроків, статей та іншого контенту
Розбираємо виведення параметрів типу, явне передавання типів і обмеження узагальнених функцій.
Generic-функція може працювати з різними типами, не втрачаючи інформацію про них.
function identity<T>(value: T): T {
return value;
}T — це параметр типу. Він описує тип, який буде відомий під час виклику функції.
Якщо передати число:
const numberResult = identity(42);TypeScript виведе:
numberResult // тип numberЯкщо передати рядок:
const textResult = identity("Привіт");Результат матиме тип string.
У цьому випадку TypeScript сам визначив, що T дорівнює:
number для identity(42);
string для identity("Привіт").
Цей процес називається виведенням параметра типу.
Розглянемо функцію, яка повертає перший елемент масиву:
function first<T>(items: T[]): T {
return items[0];
}
const firstNumber = first([10, 20, 30]);
const firstName = first(["Олена", "Андрій"]);TypeScript визначить типи так:
firstNumber // number
firstName // stringПараметр T виводиться з типу аргументу:
масив number[] дає T = number;
масив string[] дає T = string.
Generic-функція зберігає зв’язок між типом аргументу і типом результату.
Функція може мати кілька параметрів типу:
function createPair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const pair = createPair("кількість", 3);Для цього виклику TypeScript виведе:
pair // [string, number]Перший аргумент визначає T, а другий — U.
Ще один приклад:
const userData = createPair(101, true);
// тип: [number, boolean]Зазвичай TypeScript сам виводить параметри типу. Але їх можна передати явно в кутових дужках:
const result = identity<string>("TypeScript");У цьому виклику ми явно вказали:
T = stringТакож можна явно передати кілька параметрів:
const pair = createPair<string, number>("вік", 25);
// тип: [string, number]Явне передавання корисне, коли:
тип не можна однозначно вивести з аргументів;
потрібно перевірити, що аргумент відповідає конкретному типу;
потрібно зробити намір у коді очевиднішим.
Якщо вказаний тип не відповідає аргументу, TypeScript повідомить про помилку:
function identity<T>(value: T): T {
return value;
}
const value = identity<number>("42");
// Помилка: аргумент типу string не можна передати
// параметру типу numberРядок "42" — це string, а не number.
Щоб передати число, потрібно використати числове значення:
const value = identity<number>(42);TypeScript намагається знайти тип, який підходить для всіх аргументів.
function makeArray<T>(first: T, second: T): T[] {
return [first, second];
}
const values = makeArray(10, 20);
// тип: number[]Обидва аргументи мають тип number, тому результат — number[].
Якщо аргументи мають різні типи, виклик може бути помилковим:
const values = makeArray(10, "20");
// Помилка: аргументи мають несумісні типиДля різних типів потрібно використати два параметри типу:
function makeTuple<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const values = makeTuple(10, "20");
// тип: [number, string]Без обмежень generic-параметр може бути будь-яким типом. Тому TypeScript не дозволяє використовувати властивості, яких може не існувати.
Наприклад:
function getLength<T>(value: T): number {
return value.length;
}Цей код спричинить помилку, адже не кожен тип має властивість length. Наприклад, у number такої властивості немає.
Щоб повідомити TypeScript, що T повинен мати властивість length, використовується extends:
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}Тепер функція приймає значення, які мають числову властивість length.
const textLength = getLength("TypeScript");
const arrayLength = getLength([10, 20, 30]);
console.log(textLength); // 10
console.log(arrayLength); // 3Рядок і масив мають властивість length, тому обидва виклики коректні.
А такий виклик буде помилковим:
const numberLength = getLength(42);
// Помилка: number не відповідає обмеженню
// { length: number }Обмеження можна створити окремим інтерфейсом:
interface HasId {
id: number;
}
function getId<T extends HasId>(value: T): number {
return value.id;
}
const user = {
id: 7,
name: "Олена",
};
const product = {
id: 15,
price: 250,
};
console.log(getId(user)); // 7
console.log(getId(product)); // 15Функція приймає будь-який об’єкт, який має властивість id типу number. Додаткові властивості не заважають.
У generic-функції з обмеженням тип все одно виводиться з аргументу:
function logId<T extends { id: number }>(value: T): T {
console.log(value.id);
return value;
}
const result = logId({
id: 1,
title: "Книга",
});Тип T буде виведений приблизно так:
{
id: number;
title: string;
}Функція знає, що об’єкт має id, а результат зберігає також властивість title.
Для обмеженого параметра типу явно переданий тип також повинен відповідати обмеженню:
function getId<T extends { id: number }>(value: T): number {
return value.id;
}
const item = getId<{ id: number; name: string }>({
id: 3,
name: "Товар",
});Це коректно, бо тип об’єкта має id: number.
А такий тип не відповідає обмеженню:
const invalidItem = getId<{ name: string }>({
name: "Товар",
});
// Помилка: у типі немає властивості idСтворимо generic-функцію, яка знаходить елемент за його ідентифікатором.
interface Entity {
id: number;
}
function findById<T extends Entity>(
items: T[],
id: number,
): T | undefined {
return items.find((item) => item.id === id);
}
const users = [
{ id: 1, name: "Олена" },
{ id: 2, name: "Андрій" },
];
const foundUser = findById(users, 2);
console.log(foundUser);
// { id: 2, name: "Андрій" }Під час виклику TypeScript виводить T з масиву users. Тому результат має тип:
{
id: number;
name: string;
} | undefinedЗначення undefined можливе, якщо елемент із таким id не знайдено.
Безпечна перевірка результату:
if (foundUser) {
console.log(foundUser.name);
}interface HasId {
id: number;
}
function getById<T extends HasId>(
items: T[],
id: number,
): T | undefined {
return items.find((item) => item.id === id);
}
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const books = [
{ id: 1, title: "TypeScript для початківців" },
{ id: 2, title: "Основи програмування" },
];
const book = getById(books, 1);
if (book) {
console.log(book.title);
}
const metadata = pair("сторінка", 1);
console.log(metadata);
// ["сторінка", 1]
const explicitBook = getById<
{ id: number; title: string }
>(books, 2);
console.log(explicitBook);У цьому прикладі:
getById приймає лише об’єкти з числовим id.
Тип книги виводиться з масиву books.
pair виводить два різні параметри типу.
Для explicitBook тип передається явно.
Неправильно:
function getLength<T>(value: T): number {
return value.length;
}Правильно:
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}Обмеження пояснює TypeScript, яку властивість гарантовано має параметр.
Не обов’язково щоразу писати тип вручну:
const value = identity<string>("текст");У цьому випадку TypeScript і сам виведе string:
const value = identity("текст");Явний тип потрібен лише тоді, коли він справді робить код зрозумілішим або змінює бажане виведення.
identity<number>("10");
// Помилка"10" — це рядок, навіть якщо його вміст схожий на число.
identity<number>(10);
// КоректноundefinedФункція пошуку часто може не знайти значення:
const user = getById(users, 100);
console.log(user.name);
// Помилка: user може бути undefinedПотрібно спочатку перевірити результат:
if (user) {
console.log(user.name);
}Generic-функції використовують параметри типу, наприклад T.
TypeScript зазвичай сам виводить параметри типу з аргументів.
Параметри типу можна передати явно: functionName<Type>(value).
Якщо generic-функція працює з конкретними властивостями, використовуйте обмеження через extends.
Обмеження не задає один конкретний тип, а визначає мінімальний набір вимог до типу.
Кілька параметрів типу дають змогу зберігати зв’язок між кількома аргументами різних типів.
Явне передавання типів корисне, коли виведення неоднозначне або потрібно чітко зафіксувати очікуваний тип.