Ръководство за '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
на условен тип, за да се декларира променлива на тип, която може да бъде изведена (inferred) от проверявания тип. По същество, тя ви позволява да „уловите“ част от даден тип за по-късна употреба.
Основен синтаксис:
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
и ги връща като tuple (кортеж).
2. Извличане на свойства от обектни типове
infer
може да се използва и за извличане на специфични свойства от обектни типове, използвайки mapped types и условни типове:
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
. Mapped type итерира през ключовете на T
, а условният тип филтрира ключовете, които не съответстват на посочения тип.
3. Работа с Promises
Можете да извлечете типа, с който се разрешава (resolves) даден 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
, който се очаква да бъде Promise. След това типът извежда разрешения тип U
на Promise и го връща. Ако T
не е promise, той връща T. Това е вграден помощен тип в по-новите версии на TypeScript.
4. Извличане на типа от масив от Promises
Комбинирането на Awaited
и извличането на типове от масиви ви позволява да извлечете типа, с който се разрешава масив от Promises. Това е особено полезно при работа с 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
в масива, което води до tuple тип, където всеки елемент е awaited типът на съответния 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)
Да предположим, че имате функция за превод, която връща различни низове в зависимост от езиковата променлива (locale). Можете да използвате 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'); // Output: 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): Предпазителите на типове могат да се използват за стесняване на типа на дадена стойност въз основа на проверки по време на изпълнение. Това е полезно, когато трябва да обработвате различни типове въз основа на условия по време на изпълнение.
- Помощни типове (Utility Types): TypeScript предоставя богат набор от помощни типове, които могат да се справят с много често срещани задачи за манипулиране на типове без нуждата от персонализирани
infer
изрази.
Заключение
Ключовата дума infer
в TypeScript, когато се комбинира с условни типове, отключва напреднали възможности за манипулиране на типове. Тя ви позволява да извличате специфични типове от сложни структури, което ви дава възможност да пишете по-стабилен, лесен за поддръжка и типово-безопасен код. От извличане на типове на връщани стойности от функции до извличане на свойства от обектни типове, възможностите са огромни. Като разберете принципите и добрите практики, очертани в това ръководство, можете да използвате infer
в пълния му потенциал и да повишите уменията си в TypeScript. Не забравяйте да документирате типовете си, да ги тествате обстойно и да обмисляте алтернативни подходи, когато е подходящо. Овладяването на infer
ви дава възможност да пишете наистина изразителен и мощен TypeScript код, което в крайна сметка води до по-добър софтуер.