Пошук уроків, статей та іншого контенту
Застосовуємо extends, щоб обмежити допустимі типи й безпечно працювати з їхніми властивостями.
Узагальнені функції можуть працювати з різними типами:
function identity<T>(value: T): T {
return value;
}
identity(42);
identity("TypeScript");
identity({ name: "Oksana" });За замовчуванням TypeScript не знає, які властивості має T. Тому звернення до властивості всередині функції заборонене:
function getLength<T>(value: T): number {
// Помилка: властивість length не гарантована для типу T
// return value.length;
return 0;
}Щоб повідомити TypeScript, що параметр типу повинен мати певну структуру, використовують extends.
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}Тепер T може бути будь-яким типом, який має числову властивість length.
getLength("TypeScript");
getLength([1, 2, 3]);
getLength({ length: 10, description: "Значення" });А значення без властивості length не підходить:
// Помилка: number не відповідає обмеженню { length: number }
// getLength(42);extends у genericsУ контексті узагальнених типів extends означає не обов’язково успадкування класу. Воно означає, що тип повинен бути сумісним із заданим обмеженням.
function printName<T extends { name: string }>(value: T): void {
console.log(value.name);
}Функція приймає об’єкти, які мають властивість name типу string. Вони можуть також містити інші властивості:
printName({ name: "Ірина" });
printName({
name: "Олег",
age: 32,
role: "developer",
});Обмеження перевіряє лише необхідну структуру. Додаткові властивості не заважають використанню типу.
Якщо структура використовується кілька разів, її зручно винести в інтерфейс:
interface HasId {
id: number;
}
function printId<T extends HasId>(value: T): void {
console.log(`Ідентифікатор: ${value.id}`);
}
const user = {
id: 101,
name: "Марія",
};
const product = {
id: 202,
title: "Клавіатура",
};
printId(user);
printId(product);І user, і product мають властивість id типу number, тому обидва об’єкти відповідають HasId.
Обмеження можна будувати не лише на об’єктах. Наприклад, параметр типу можна обмежити лише рядками:
function toUpperCase<T extends string>(value: T): string {
return value.toUpperCase();
}
toUpperCase("hello");
// Помилка: number не відповідає обмеженню string
// toUpperCase(42);Або дозволити лише числові типи:
function double<T extends number>(value: T): number {
return value * 2;
}
console.log(double(21));На практиці для простих функцій часто достатньо написати value: string або value: number. Обмеження особливо корисні, коли тип використовується як частина складнішої узагальненої конструкції.
Іноді потрібно заборонити примітивні значення й дозволити лише об’єкти:
function clone<T extends object>(value: T): T {
return { ...value };
}
const original = {
name: "Андрій",
active: true,
};
const copy = clone(original);
// Помилка: примітивне значення не відповідає обмеженню object
// clone(42);Однак T extends object саме по собі не гарантує наявність конкретних властивостей. TypeScript усе ще не дозволить написати value.name, тому що не кожен об’єкт має name.
function processValue<T extends object>(value: T): void {
// Помилка: властивість name не гарантована типом object
// console.log(value.name);
}Якщо функції потрібна певна властивість, її треба явно вказати в обмеженні:
function processNamedValue<T extends { name: string }>(value: T): void {
console.log(value.name);
}Одне з найпрактичніших застосувань extends — безпечне отримання властивості за її ключем.
function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
const account = {
id: 7,
username: "developer",
isActive: true,
};
const id = getProperty(account, "id");
const username = getProperty(account, "username");
const isActive = getProperty(account, "isActive");
console.log(id);
console.log(username);
console.log(isActive);Розглянемо параметри функції:
T — тип переданого об’єкта;
keyof T — об’єднання всіх ключів типу T;
K extends keyof T — K може бути лише одним із ключів T;
T[K] — тип значення за ключем K.
Для об’єкта account TypeScript визначає приблизно таку структуру:
type Account = {
id: number;
username: string;
isActive: boolean;
};Тому keyof Account дорівнює:
"id" | "username" | "isActive"Спроба передати невідомий ключ викликає помилку:
// Помилка: email не є ключем об'єкта account
// getProperty(account, "email");Цей підхід безпечніший за використання простого string, оскільки помилковий ключ виявляється під час перевірки типів.
Обмеження одного параметра може залежати від іншого параметра типу.
function setProperty<T, K extends keyof T>(
object: T,
key: K,
value: T[K],
): T {
return {
...object,
[key]: value,
};
}
const settings = {
theme: "light",
notifications: true,
};
const updatedSettings = setProperty(settings, "theme", "dark");
const updatedNotifications = setProperty(
settings,
"notifications",
false,
);
console.log(updatedSettings);
console.log(updatedNotifications);У цьому прикладі тип value залежить від переданого ключа:
для ключа "theme" потрібен рядок;
для ключа "notifications" потрібне булеве значення.
Тому такі виклики є помилковими:
// Помилка: для theme потрібен string
// setProperty(settings, "theme", false);
// Помилка: для notifications потрібен boolean
// setProperty(settings, "notifications", "yes");Обмеження можна використовувати для функцій, які працюють із масивами та іншими колекціями.
function first<T extends readonly unknown[]>(items: T): T[number] | undefined {
return items[0];
}
const numbers = [10, 20, 30];
const firstNumber = first(numbers);
const names = ["Анна", "Богдан"];
const firstName = first(names);
console.log(firstNumber);
console.log(firstName);Обмеження T extends readonly unknown[] гарантує, що значення можна індексувати як масив або кортеж. T[number] означає тип елементів цієї колекції.
Для функції, якій достатньо лише властивості length, не потрібно обмежувати значення саме масивом:
function describeLength<T extends { length: number }>(value: T): string {
return `Довжина: ${value.length}`;
}
console.log(describeLength("TypeScript"));
console.log(describeLength([1, 2, 3]));Такий варіант ширший: він працює з будь-яким типом, що має потрібну властивість.
TypeScript зазвичай сам виводить параметри типу з аргументів функції:
function getId<T extends { id: number }>(value: T): number {
return value.id;
}
const task = {
id: 15,
title: "Перевірити типи",
completed: false,
};
const taskId = getId(task);У цьому випадку TypeScript виводить T як тип об’єкта task. Нам не потрібно передавати тип явно.
Тип можна вказати вручну, якщо це потрібно:
interface User {
id: number;
name: string;
}
const user: User = {
id: 1,
name: "Софія",
};
const userId = getId<User>(user);Але явне зазначення типу не повинно обходити обмеження:
// Помилка: User не має числової властивості id
// getId<{ id: string }>({ id: "abc" });Обмеження параметрів типу доречні, коли узагальнена функція повинна:
звертатися до конкретної властивості аргументу;
гарантувати наявність певного набору полів;
працювати лише з ключами конкретного об’єкта;
пов’язувати тип ключа з типом відповідного значення;
приймати лише масиви, рядки або інші типи з певною структурою.
Без обмеження TypeScript змушений розглядати T як повністю невідомий тип. Обмеження додає необхідні гарантії, але зберігає узагальненість функції.
function getName<T>(value: T): string {
// Помилка: властивість name не гарантована для T
// return value.name;
return "";
}Потрібно додати обмеження:
function getName<T extends { name: string }>(value: T): string {
return value.name;
}T extends object не повідомляє TypeScript про конкретні властивості:
function printTitle<T extends object>(value: T): void {
// Помилка: title не гарантована властивість object
// console.log(value.title);
}Якщо потрібна властивість title, її слід вказати явно:
function printTitle<T extends { title: string }>(value: T): void {
console.log(value.title);
}string замість keyofНебезпечний варіант:
function getValue<T>(object: T, key: string): unknown {
return (object as Record<string, unknown>)[key];
}Такий код дозволяє передати будь-який рядок, навіть якщо ключа немає в об’єкті.
Безпечніший варіант:
function getValue<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}extends не перетворює значення на інший тип і не додає властивості під час виконання. Воно лише перевіряє, чи відповідає тип заданим вимогам під час компіляції.
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}Ця функція не створює властивість length. Вона може працювати лише з переданим значенням, у якого така властивість уже є.
T extends Constraint обмежує допустимі типи для параметра T.
Обмеження описує мінімальну структуру, яку повинен мати тип.
Додаткові властивості не заважають типу відповідати обмеженню.
T extends { length: number } дозволяє безпечно звертатися до length.
T extends keyof U обмежує значення ключами типу U.
Комбінація K extends keyof T і T[K] зберігає зв’язок між ключем та типом його значення.
Обмеження перевіряються TypeScript під час компіляції й не додають поведінки під час виконання.