Пошук уроків, статей та іншого контенту
Поєднуємо кілька параметрів типу в одній функції або структурі та передаємо між ними залежності.
Універсальна функція може працювати не з одним, а з кількома параметрами типу. Це дає змогу описати зв’язок між різними значеннями, аргументами або структурами даних.
function makePair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const pair = makePair("TypeScript", 5);
// Тип: [string, number]
console.log(pair);У цьому прикладі:
T описує тип першого аргументу;
U описує тип другого аргументу;
результатом є кортеж [T, U].
Під час виклику TypeScript сам виводить:
T = string
U = numberТому зазвичай не потрібно передавати параметри типу вручну.
Параметри типу можна вказати явно в кутових дужках:
function makePair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const pair = makePair<string, number>("версія", 5);
console.log(pair[0].toUpperCase());
console.log(pair[1].toFixed(1));Порядок типів відповідає порядку параметрів функції:
makePair<string, number>("версія", 5);Тут:
T — string;
U — number.
Якщо типи не відповідають аргументам, TypeScript повідомить про помилку:
// Помилка: number не можна передати замість string
// const invalidPair = makePair<string, number>(10, "версія");Кілька параметрів типу особливо корисні, коли один тип залежить від іншого.
Наприклад, функція може отримувати об’єкт і назву його властивості. Тип назви властивості повинен залежати від типу самого об’єкта:
function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
const user = {
id: 42,
name: "Олена",
active: true,
};
const userName = getProperty(user, "name");
const userId = getProperty(user, "id");
console.log(userName.toUpperCase());
console.log(userId.toFixed());Тут використано два параметри типу:
T — тип усього об’єкта;
K — тип ключа цього об’єкта.
Обмеження K extends keyof T означає: K може бути лише одним із ключів T.
Для об’єкта user TypeScript визначає:
keyof typeof user
// "id" | "name" | "active"Тому коректними є такі виклики:
getProperty(user, "id");
getProperty(user, "name");
getProperty(user, "active");А такого ключа в об’єкті немає:
// Помилка компіляції
// getProperty(user, "email");Тип результату також залежить від ключа:
const name = getProperty(user, "name");
// string
const id = getProperty(user, "id");
// number
const active = getProperty(user, "active");
// booleanЦе безпечніше, ніж повертати значення типу any, адже TypeScript зберігає точний тип властивості.
Кілька параметрів типу можна використовувати не лише у функціях, а й у типах та інтерфейсах.
interface Result<T, E> {
data?: T;
error?: E;
}
function createSuccess<T>(data: T): Result<T, never> {
return { data };
}
function createFailure<E>(error: E): Result<never, E> {
return { error };
}
const success = createSuccess({ id: 1, name: "Марія" });
const failure = createFailure("Користувача не знайдено");
console.log(success.data);
console.log(failure.error);Result<T, E> описує результат, який може містити:
T — тип успішних даних;
E — тип помилки.
Наприклад:
type User = {
id: number;
name: string;
};
type ApiError = {
message: string;
statusCode: number;
};
const result: Result<User, ApiError> = {
data: {
id: 1,
name: "Іван",
},
};Тепер поле data, якщо воно присутнє, має тип User, а поле error — тип ApiError.
Обмеження можна застосовувати до кожного параметра типу окремо.
function combineObjects<
T extends object,
U extends object
>(first: T, second: U): T & U {
return {
...first,
...second,
};
}
const combined = combineObjects(
{ id: 1 },
{ name: "Анна" }
);
console.log(combined.id);
console.log(combined.name);У цьому прикладі:
T повинен бути об’єктом;
U повинен бути об’єктом;
результат має тип T & U.
Оператор & об’єднує властивості двох типів. Тому результат містить і id, і name.
Виклик із примітивами буде помилковим:
// Помилка: аргументи не відповідають обмеженню object
// combineObjects(10, "text");Кілька параметрів типу допомагають гарантувати, що два аргументи мають узгоджені типи.
function repeatValue<T>(value: T, count: number): T[] {
return Array.from({ length: count }, () => value);
}
const numbers = repeatValue(7, 3);
const words = repeatValue("hello", 2);
console.log(numbers);
console.log(words);У цій функції другий параметр має звичайний тип number, а тип результату залежить від першого параметра.
Складніший приклад — функція, яка перетворює значення одного типу на значення іншого типу:
function transform<T, U>(
value: T,
converter: (value: T) => U
): U {
return converter(value);
}
const length = transform("TypeScript", (text) => text.length);
const formatted = transform(42, (number) => `Значення: ${number}`);
console.log(length);
console.log(formatted);Тут:
T — тип початкового значення;
U — тип результату перетворення;
функція converter отримує T і повертає U.
TypeScript виводить типи автоматично:
// T = string, U = number
transform("TypeScript", (text) => text.length);
// T = number, U = string
transform(42, (number) => `Значення: ${number}`);Такий шаблон часто використовують для перетворення елементів, нормалізації даних і створення адаптерів.
Для параметра типу можна вказати значення за замовчуванням.
interface Box<T, Metadata = undefined> {
value: T;
metadata: Metadata;
}
const numberBox: Box<number> = {
value: 100,
metadata: undefined,
};
const userBox: Box<
string,
{ source: string }
> = {
value: "user-1",
metadata: {
source: "database",
},
};
console.log(numberBox);
console.log(userBox);У Box<T, Metadata = undefined>:
T потрібно вказати або вивести;
Metadata за замовчуванням дорівнює undefined.
Якщо потрібен інший тип метаданих, його можна передати явно.
Параметр із типом за замовчуванням має розташовуватися після обов’язкових параметрів типу.
TypeScript намагається вивести всі параметри типу з аргументів:
function createEntry<K, V>(key: K, value: V): { key: K; value: V } {
return { key, value };
}
const entry = createEntry("theme", "dark");
// { key: string; value: string }Явне зазначення типів потрібне, коли:
TypeScript не може коректно вивести тип;
потрібно зафіксувати ширший або конкретніший тип;
типи повинні бути зрозумілими з самого коду.
const explicitEntry = createEntry<string, string>(
"theme",
"dark"
);У більшості простих випадків краще покладатися на виведення типів, щоб не дублювати інформацію.
Нижче наведено приклад функції, яка створює словник із масиву об’єктів. Один параметр типу описує елементи масиву, а інший — ключ, за яким вони індексуються.
type Product = {
id: number;
title: string;
price: number;
};
function indexBy<T, K extends keyof T>(
items: T[],
key: K
): Map<T[K], T> {
const result = new Map<T[K], T>();
for (const item of items) {
result.set(item[key], item);
}
return result;
}
const products: Product[] = [
{ id: 1, title: "Клавіатура", price: 2500 },
{ id: 2, title: "Миша", price: 1200 },
];
const productsById = indexBy(products, "id");
const productsByTitle = indexBy(products, "title");
console.log(productsById.get(1));
console.log(productsByTitle.get("Миша"));
// Помилка: "category" не є ключем Product
// const productsByCategory = indexBy(products, "category");У функції indexBy:
T — тип елемента масиву, у цьому випадку Product;
K extends keyof T — ключ, який повинен існувати в Product;
T[K] — тип значення за цим ключем;
Map<T[K], T> зберігає елементи за значеннями вибраної властивості.
Для ключа "id" тип мапи буде приблизно таким:
Map<number, Product>Для ключа "title":
Map<string, Product>Таким чином, тип ключа мапи залежить від переданої властивості.
Параметри типу передаються в тому самому порядку, у якому вони оголошені:
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const correct = pair<string, number>("text", 10);
// Помилка: очікується string, але передано number
// const incorrect = pair<string, number>(10, "text");Якщо зв’язок між параметрами не описаний, TypeScript не зможе захистити від некоректних значень:
function getValue<T>(object: T, key: string) {
// Такий ключ не перевіряється як ключ саме цього об’єкта
return (object as Record<string, unknown>)[key];
}Краще пов’язати ключ з об’єктом:
function getSafeValue<T, K extends keyof T>(
object: T,
key: K
): T[K] {
return object[key];
}any замість залежних типівany вимикає перевірку типів:
function unsafeGet(object: any, key: any): any {
return object[key];
}Універсальна версія з K extends keyof T зберігає залежність між об’єктом, ключем і результатом:
function safeGet<T, K extends keyof T>(
object: T,
key: K
): T[K] {
return object[key];
}Кожен параметр типу повинен мати окреме призначення. Якщо кілька параметрів завжди мають однаковий тип і не створюють додаткового зв’язку, їх краще не додавати без потреби.
Погано:
function logValue<T, U>(value: T): void {
console.log(value);
}U тут не використовується, тому він не додає функції корисної інформації.
Краще:
function logValue<T>(value: T): void {
console.log(value);
}Функція або структура може мати кілька параметрів типу: T, U, K тощо.
Кожен параметр типу може описувати окреме значення або частину структури.
Параметри типу можуть залежати один від одного.
Запис K extends keyof T обмежує K ключами типу T.
Запис T[K] отримує тип властивості K у типі T.
Обмеження extends допомагають пов’язати параметри типу та уникнути некоректних викликів.
Параметри типу можна передавати явно, але зазвичай TypeScript виводить їх автоматично.
Не слід додавати невикористані параметри типу або замінювати залежні типи на any.