Un ghid cuprinzător despre cuvântul cheie 'infer' din TypeScript, explicând cum să-l folosești cu tipuri condiționale pentru extracție și manipulare puternică a tipurilor.
Stăpânirea lui TypeScript Infer: Extracția Tipului Condițional pentru Manipularea Avansată a Tipurilor
Sistemul de tipuri TypeScript este incredibil de puternic, permițând dezvoltatorilor să creeze aplicații robuste și ușor de întreținut. Una dintre caracteristicile cheie care permit această putere este cuvântul cheie infer
, utilizat în combinație cu tipurile condiționale. Această combinație oferă un mecanism pentru extragerea tipurilor specifice din structuri de tip complexe. Această postare de blog analizează în profunzime cuvântul cheie infer
, explicându-i funcționalitatea și prezentând cazuri de utilizare avansate. Vom explora exemple practice aplicabile diverselor scenarii de dezvoltare software, de la interacțiunea API până la manipularea complexă a structurilor de date.
Ce sunt Tipurile Condiționale?
Înainte de a ne adânci în infer
, haideți să trecem rapid în revistă tipurile condiționale. Tipurile condiționale din TypeScript vă permit să definiți un tip pe baza unei condiții, similar cu un operator ternar în JavaScript. Sintaxa de bază este:
T extends U ? X : Y
Aceasta se citește astfel: "Dacă tipul T
este atribuibil tipului U
, atunci tipul este X
; altfel, tipul este Y
."
Exemplu:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Introducere în Cuvântul Cheie infer
Cuvântul cheie infer
este utilizat în cadrul clauzei extends
a unui tip condițional pentru a declara o variabilă de tip care poate fi dedusă din tipul verificat. În esență, vă permite să "capturați" o parte a unui tip pentru utilizare ulterioară.
Sintaxă de Bază:
type MyType<T> = T extends (infer U) ? U : never;
În acest exemplu, dacă T
este atribuibil unui anumit tip, TypeScript va încerca să deducă tipul lui U
. Dacă inferența are succes, tipul va fi U
; altfel, va fi never
.
Exemple Simple de infer
1. Deducerea Tipului Returnat al unei Funcții
Un caz de utilizare comun este deducerea tipului returnat al unei funcții:
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
În acest exemplu, ReturnType<T>
ia un tip de funcție T
ca intrare. Verifică dacă T
este atribuibil unei funcții care acceptă orice argumente și returnează o valoare. Dacă este, deduce tipul returnat ca R
și îl returnează. Altfel, returnează any
.
2. Deducerea Tipului Elementului de Tablou
Un alt scenariu util este extragerea tipului de element dintr-un tablou:
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
Aici, ArrayElementType<T>
verifică dacă T
este un tip de tablou. Dacă este, deduce tipul elementului ca U
și îl returnează. Dacă nu, returnează never
.
Cazuri Avansate de Utilizare a lui infer
1. Deducerea Parametrilor unui Constructor
Puteți utiliza infer
pentru a extrage tipurile de parametri ale unei funcții constructor:
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]
În acest caz, ConstructorParameters<T>
ia un tip de funcție constructor T
. Deducă tipurile parametrilor constructorului ca P
și le returnează ca un tuplu.
2. Extragerea Proprietăților din Tipurile de Obiecte
infer
poate fi, de asemenea, utilizat pentru a extrage proprietăți specifice din tipurile de obiecte utilizând tipuri mapate și tipuri condiționale:
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; }
//An interface representing geographic coordinates.
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; }
Aici, PickByType<T, K, U>
creează un tip nou care include doar proprietățile lui T
(cu chei în K
) ale căror valori sunt atribuibile tipului U
. Tipul mapat iterează asupra cheilor lui T
, iar tipul condițional filtrează cheile care nu se potrivesc cu tipul specificat.
3. Lucrul cu Promisiuni
Puteți deduce tipul rezolvat al unei 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[]
Tipul Awaited<T>
ia un tip T
, care se așteaptă să fie o Promise. Tipul deduce apoi tipul rezolvat U
al Promise-ului și îl returnează. Dacă T
nu este o promisiune, returnează T. Acesta este un tip de utilitate încorporat în versiunile mai noi de TypeScript.
4. Extragerea Tipului unui Tablou de Promisiuni
Combinarea Awaited
și a inferenței tipului de tablou vă permite să deduceți tipul rezolvat de un tablou de Promisiuni. Acest lucru este util mai ales atunci când aveți de-a face cu 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]
Acest exemplu definește mai întâi două funcții asincrone, getUSDRate
și getEURRate
, care simulează preluarea cursurilor de schimb. Tipul de utilitate PromiseArrayReturnType
extrage apoi tipul rezolvat din fiecare Promise
din tablou, rezultând un tip tuplu în care fiecare element este tipul așteptat al Promise-ului corespunzător.
Exemple Practice în Diferite Domenii
1. Aplicație de Comerț Electronic
Luați în considerare o aplicație de comerț electronic în care preluați detaliile produsului dintr-un API. Puteți utiliza infer
pentru a extrage tipul datelor despre produs:
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> {
// Simulate API call
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);
În acest exemplu, definim o interfață Product
și o funcție fetchProduct
care preia detaliile produsului dintr-un API. Folosim Awaited
și ReturnType
pentru a extrage tipul Product
din tipul returnat al funcției fetchProduct
, permițându-ne să verificăm tipul funcției displayProductDetails
.
2. Internaționalizare (i18n)
Să presupunem că aveți o funcție de traducere care returnează șiruri diferite în funcție de localizare. Puteți utiliza infer
pentru a extrage tipul returnat al acestei funcții pentru siguranța tipului:
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!
Aici, TranslationType
este dedus ca fiind interfața Translations
, asigurându-se că funcția greetUser
are informațiile corecte de tip pentru accesarea șirurilor traduse.
3. Gestionarea Răspunsurilor API
Când lucrați cu API-uri, structura răspunsului poate fi complexă. infer
vă poate ajuta să extrageți tipuri de date specifice din răspunsurile API imbricate:
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>> {
// Simulate API call
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);
}
});
În acest exemplu, definim o interfață ApiResponse
și o interfață UserData
. Folosim infer
și indexarea tipurilor pentru a extrage UserProfileType
din răspunsul API, asigurându-ne că funcția displayUserProfile
primește tipul corect.
Cele Mai Bune Practici Pentru Utilizarea lui infer
- Păstrați-l Simplu: Utilizați
infer
numai atunci când este necesar. Utilizarea excesivă a acestuia poate face codul mai greu de citit și înțeles. - Documentați-vă Tipurile: Adăugați comentarii pentru a explica ce fac tipurile dvs. condiționale și instrucțiunile
infer
. - Testați-vă Tipurile: Utilizați verificarea tipurilor TypeScript pentru a vă asigura că tipurile dvs. se comportă așa cum vă așteptați.
- Luați în Considerare Performanța: Tipurile condiționale complexe pot afecta uneori timpul de compilare. Fiți atenți la complexitatea tipurilor dvs.
- Utilizați Tipurile Utilitare: TypeScript oferă mai multe tipuri utilitare încorporate (de exemplu,
ReturnType
,Awaited
) care vă pot simplifica codul și pot reduce nevoia de instrucțiuniinfer
personalizate.
Capcane Comune
- Inferență Incorectă: Uneori, TypeScript ar putea deduce un tip care nu este cel pe care îl așteptați. Verificați de două ori definițiile și condițiile tipului.
- Dependențe Circulare: Aveți grijă când definiți tipuri recursive utilizând
infer
, deoarece acestea pot duce la dependențe circulare și erori de compilare. - Tipuri Prea Complexe: Evitați crearea de tipuri condiționale prea complexe, care sunt dificil de înțeles și de întreținut. Împărțiți-le în tipuri mai mici, mai ușor de gestionat.
Alternative la infer
În timp ce infer
este un instrument puternic, există situații în care abordările alternative ar putea fi mai adecvate:
- Aserțiuni de Tip: În unele cazuri, puteți utiliza aserțiuni de tip pentru a specifica explicit tipul unei valori în loc să îl deduceți. Cu toate acestea, fiți precauți cu aserțiunile de tip, deoarece acestea pot ocoli verificarea tipului.
- Gărzi de Tip: Gărzile de tip pot fi utilizate pentru a restrânge tipul unei valori pe baza verificărilor runtime. Acest lucru este util atunci când trebuie să gestionați diferite tipuri pe baza condițiilor runtime.
- Tipuri Utilitare: TypeScript oferă un set bogat de tipuri utilitare care pot gestiona multe sarcini comune de manipulare a tipurilor, fără a fi nevoie de instrucțiuni
infer
personalizate.
Concluzie
Cuvântul cheie infer
din TypeScript, atunci când este combinat cu tipurile condiționale, deblochează capabilități avansate de manipulare a tipurilor. Vă permite să extrageți tipuri specifice din structuri de tip complexe, permițându-vă să scrieți cod mai robust, mai ușor de întreținut și mai sigur pentru tipuri. De la deducerea tipurilor returnate ale funcțiilor până la extragerea proprietăților din tipurile de obiecte, posibilitățile sunt vaste. Înțelegând principiile și cele mai bune practici prezentate în acest ghid, puteți valorifica infer
la potențialul său maxim și vă puteți îmbunătăți abilitățile TypeScript. Nu uitați să vă documentați tipurile, să le testați temeinic și să luați în considerare abordări alternative atunci când este cazul. Stăpânirea lui infer
vă permite să scrieți cod TypeScript cu adevărat expresiv și puternic, ceea ce duce în cele din urmă la un software mai bun.