Вичерпний посібник з ключового слова 'infer' у TypeScript, що пояснює його використання з умовними типами для потужного видобування та маніпуляції типами.
Опанування TypeScript Infer: видобування умовних типів для розширених маніпуляцій з типами
Система типів TypeScript неймовірно потужна, що дозволяє розробникам створювати надійні та підтримувані застосунки. Однією з ключових особливостей, що забезпечує цю потужність, є ключове слово infer
, яке використовується разом з умовними типами. Ця комбінація надає механізм для видобування конкретних типів зі складних структур типів. Ця стаття глибоко розглядає ключове слово infer
, пояснюючи його функціональність та демонструючи розширені випадки використання. Ми розглянемо практичні приклади, застосовні до різноманітних сценаріїв розробки програмного забезпечення, від взаємодії з API до маніпуляції складними структурами даних.
Що таке умовні типи?
Перш ніж ми зануримося в infer
, давайте швидко розглянемо умовні типи. Умовні типи в TypeScript дозволяють визначати тип на основі умови, подібно до тернарного оператора в JavaScript. Базовий синтаксис такий:
T extends U ? X : Y
Це читається так: «Якщо тип T
може бути присвоєний типу U
, то тип буде X
; в іншому випадку — Y
.»
Приклад:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Представляємо ключове слово infer
Ключове слово infer
використовується в межах умови extends
умовного типу для оголошення змінної типу, яку можна вивести з перевірюваного типу. По суті, це дозволяє «захопити» частину типу для подальшого використання.
Базовий синтаксис:
type MyType<T> = T extends (infer U) ? U : never;
У цьому прикладі, якщо T
можна присвоїти якомусь типу, TypeScript спробує вивести тип U
. Якщо виведення успішне, тип буде U
; в іншому випадку — never
.
Прості приклади використання infer
1. Виведення типу, що повертається функцією
Поширеним випадком використання є виведення типу, що повертається функцією:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number
function greet(name: string): string {
return `Hello, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
У цьому прикладі ReturnType<T>
приймає тип функції T
. Він перевіряє, чи можна присвоїти T
функції, яка приймає будь-які аргументи та повертає значення. Якщо так, він виводить тип, що повертається, як R
і повертає його. В іншому випадку, він повертає any
.
2. Виведення типу елемента масиву
Інший корисний сценарій — видобування типу елемента з масиву:
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never
Тут ArrayElementType<T>
перевіряє, чи є T
типом масиву. Якщо так, він виводить тип елемента як U
і повертає його. Якщо ні, він повертає never
.
Розширені випадки використання infer
1. Виведення параметрів конструктора
Ви можете використовувати infer
для видобування типів параметрів функції-конструктора:
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Person {
constructor(public name: string, public age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]
class Point {
constructor(public x: number, public y: number) {}
}
type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]
У цьому випадку ConstructorParameters<T>
приймає тип функції-конструктора T
. Він виводить типи параметрів конструктора як P
і повертає їх у вигляді кортежу.
2. Видобування властивостей з об'єктних типів
infer
також можна використовувати для видобування конкретних властивостей з об'єктних типів за допомогою зіставлених та умовних типів:
type PickByType<T, K extends keyof T, U> = {
[P in K as T[P] extends U ? P : never]: T[P];
};
interface User {
id: number;
name: string;
age: number;
email: string;
isActive: boolean;
}
type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }
type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }
//Інтерфейс, що представляє географічні координати.
interface GeoCoordinates {
latitude: number;
longitude: number;
altitude: number;
country: string;
city: string;
timezone: string;
}
type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }
Тут PickByType<T, K, U>
створює новий тип, який включає лише ті властивості T
(з ключами в K
), значення яких можна присвоїти типу U
. Зіставлений тип ітерує по ключах T
, а умовний тип відфільтровує ключі, які не відповідають вказаному типу.
3. Робота з промісами (Promises)
Ви можете вивести тип, що повертається промісом (Promise
) після його виконання:
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'Data from API';
}
type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string
async function fetchNumbers(): Promise<number[]> {
return [1, 2, 3];
}
type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]
Тип Awaited<T>
приймає тип T
, який, як очікується, є промісом. Потім тип виводить тип U
, що повертається промісом, і повертає його. Якщо T
не є промісом, він повертає T. Це вбудований утилітний тип у новіших версіях TypeScript.
4. Видобування типу з масиву промісів
Комбінуючи Awaited
та виведення типу масиву, можна вивести тип, що повертається масивом промісів. Це особливо корисно при роботі з Promise.all
.
type PromiseArrayReturnType<T extends Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};
async function getUSDRate(): Promise<number> {
return 0.0069;
}
async function getEURRate(): Promise<number> {
return 0.0064;
}
const rates = [getUSDRate(), getEURRate()];
type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]
У цьому прикладі спочатку визначаються дві асинхронні функції, getUSDRate
та getEURRate
, які імітують отримання обмінних курсів. Утилітний тип PromiseArrayReturnType
потім видобуває тип, що повертається кожним Promise
у масиві, результатом чого є тип кортежу, де кожен елемент є типом відповідного промісу після його виконання.
Практичні приклади з різних галузей
1. Застосунок для електронної комерції
Розглянемо застосунок для електронної комерції, де ви отримуєте деталі продукту з API. Ви можете використовувати infer
для видобування типу даних продукту:
interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
category: string;
rating: number;
countryOfOrigin: string;
}
async function fetchProduct(productId: number): Promise<Product> {
// Імітація виклику API
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Example Product',
price: 29.99,
description: 'A sample product',
imageUrl: 'https://example.com/image.jpg',
category: 'Electronics',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Product Name: ${product.name}`);
console.log(`Price: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
У цьому прикладі ми визначаємо інтерфейс Product
та функцію fetchProduct
, яка отримує деталі продукту з API. Ми використовуємо Awaited
та ReturnType
для видобування типу Product
з типу, що повертається функцією fetchProduct
, що дозволяє нам перевіряти типи у функції displayProductDetails
.
2. Інтернаціоналізація (i18n)
Припустимо, у вас є функція перекладу, яка повертає різні рядки залежно від локалі. Ви можете використовувати infer
для видобування типу, що повертається цією функцією, для безпеки типів:
interface Translations {
greeting: string;
farewell: string;
welcomeMessage: (name: string) => string;
}
const enTranslations: Translations = {
greeting: 'Hello',
farewell: 'Goodbye',
welcomeMessage: (name: string) => `Welcome, ${name}!`,
};
const frTranslations: Translations = {
greeting: 'Bonjour',
farewell: 'Au revoir',
welcomeMessage: (name: string) => `Bienvenue, ${name}!`,
};
function getTranslation(locale: 'en' | 'fr'): Translations {
return locale === 'en' ? enTranslations : frTranslations;
}
type TranslationType = ReturnType<typeof getTranslation>;
function greetUser(locale: 'en' | 'fr', name: string) {
const translations = getTranslation(locale);
console.log(translations.welcomeMessage(name));
}
greetUser('fr', 'Jean'); // Вивід: Bienvenue, Jean!
Тут тип TranslationType
виводиться як інтерфейс Translations
, що гарантує, що функція greetUser
має правильну інформацію про типи для доступу до перекладених рядків.
3. Обробка відповідей API
При роботі з API структура відповіді може бути складною. infer
може допомогти видобути конкретні типи даних із вкладених відповідей API:
interface ApiResponse<T> {
status: number;
data: T;
message?: string;
}
interface UserData {
id: number;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
country: string;
language: string;
}
}
async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
// Імітація виклику API
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: 200,
data: {
id: userId,
username: 'johndoe',
email: 'john.doe@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
country: 'USA',
language: 'en'
}
}
});
}, 500);
});
}
type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;
type UserProfileType = UserApiResponse['data']['profile'];
function displayUserProfile(profile: UserProfileType) {
console.log(`Name: ${profile.firstName} ${profile.lastName}`);
console.log(`Country: ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
У цьому прикладі ми визначаємо інтерфейс ApiResponse
та інтерфейс UserData
. Ми використовуємо infer
та індексацію типів для видобування UserProfileType
з відповіді API, що гарантує, що функція displayUserProfile
отримує правильний тип.
Найкращі практики використання infer
- Будьте простішими: Використовуйте
infer
лише за необхідності. Надмірне використання може ускладнити читання та розуміння вашого коду. - Документуйте свої типи: Додавайте коментарі для пояснення того, що роблять ваші умовні типи та вирази з
infer
. - Тестуйте свої типи: Використовуйте перевірку типів TypeScript, щоб переконатися, що ваші типи поводяться так, як очікується.
- Враховуйте продуктивність: Складні умовні типи іноді можуть впливати на час компіляції. Будьте уважні до складності ваших типів.
- Використовуйте утилітні типи: TypeScript надає кілька вбудованих утилітних типів (наприклад,
ReturnType
,Awaited
), які можуть спростити ваш код і зменшити потребу у власних виразах зinfer
.
Поширені помилки
- Неправильне виведення: Іноді TypeScript може вивести тип, який не відповідає вашим очікуванням. Двічі перевіряйте визначення типів та умови.
- Циклічні залежності: Будьте обережні при визначенні рекурсивних типів з використанням
infer
, оскільки це може призвести до циклічних залежностей та помилок компіляції. - Надмірно складні типи: Уникайте створення надмірно складних умовних типів, які важко зрозуміти та підтримувати. Розбивайте їх на менші, більш керовані типи.
Альтернативи infer
Хоча infer
є потужним інструментом, існують ситуації, коли альтернативні підходи можуть бути більш доцільними:
- Твердження типів (Type Assertions): У деяких випадках ви можете використовувати твердження типів для явного вказання типу значення замість його виведення. Однак будьте обережні з твердженнями типів, оскільки вони можуть обійти перевірку типів.
- Захисники типів (Type Guards): Захисники типів можна використовувати для звуження типу значення на основі перевірок під час виконання. Це корисно, коли вам потрібно обробляти різні типи залежно від умов під час виконання.
- Утилітні типи: TypeScript надає багатий набір утилітних типів, які можуть впоратися з багатьма поширеними завданнями маніпуляції типами без необхідності у власних виразах з
infer
.
Висновок
Ключове слово infer
у TypeScript, у поєднанні з умовними типами, відкриває розширені можливості маніпуляції типами. Воно дозволяє видобувати конкретні типи зі складних структур, даючи змогу писати більш надійний, підтримуваний та типобезпечний код. Від виведення типів, що повертаються функціями, до видобування властивостей з об'єктних типів — можливості величезні. Розуміючи принципи та найкращі практики, викладені в цьому посібнику, ви зможете використовувати infer
на повну потужність і підвищити свої навички TypeScript. Не забувайте документувати свої типи, ретельно їх тестувати та розглядати альтернативні підходи, коли це доречно. Опанування infer
дає вам змогу писати справді виразний і потужний код на TypeScript, що в кінцевому підсумку призводить до кращого програмного забезпечення.