Sveobuhvatan vodič za TypeScript ključnu riječ 'infer', koji objašnjava kako je koristiti s uvjetnim tipovima za moćno izdvajanje i manipulaciju tipovima, uključujući napredne primjere upotrebe.
Ovladavanje TypeScript Infer: Izdvajanje uvjetnih tipova za naprednu manipulaciju tipovima
TypeScriptov sustav tipova je nevjerojatno moćan, omogućujući developerima stvaranje robusnih i održivih aplikacija. Jedna od ključnih značajki koja omogućuje tu moć je ključna riječ infer
koja se koristi u kombinaciji s uvjetnim tipovima. Ova kombinacija pruža mehanizam za izdvajanje specifičnih tipova iz složenih struktura tipova. Ovaj blog post duboko zaranja u ključnu riječ infer
, objašnjavajući njezinu funkcionalnost i prikazujući napredne primjere upotrebe. Istražit ćemo praktične primjere primjenjive u različitim scenarijima razvoja softvera, od interakcije s API-jem do manipulacije složenim strukturama podataka.
Što su uvjetni tipovi?
Prije nego što zaronimo u infer
, brzo ponovimo uvjetne tipove. Uvjetni tipovi u TypeScriptu omogućuju vam definiranje tipa na temelju uvjeta, slično ternarnom operatoru u JavaScriptu. Osnovna sintaksa je:
T extends U ? X : Y
Ovo se čita kao: "Ako je tip T
dodjeljiv tipu U
, onda je tip X
; inače, tip je Y
."
Primjer:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Uvođenje ključne riječi infer
Ključna riječ infer
koristi se unutar extends
klauzule uvjetnog tipa za deklariranje varijable tipa koja se može inferirati (zaključiti) iz tipa koji se provjerava. U suštini, omogućuje vam da "uhvatite" dio tipa za kasniju upotrebu.
Osnovna sintaksa:
type MyType<T> = T extends (infer U) ? U : never;
U ovom primjeru, ako je T
dodjeljiv nekom tipu, TypeScript će pokušati inferirati tip od U
. Ako je inferencija uspješna, tip će biti U
; inače, bit će never
.
Jednostavni primjeri upotrebe infer
1. Inferiranje povratnog tipa funkcije
Čest slučaj upotrebe je inferiranje povratnog tipa funkcije:
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
U ovom primjeru, ReturnType<T>
uzima tip funkcije T
kao ulaz. Provjerava je li T
dodjeljiv funkciji koja prihvaća bilo koje argumente i vraća vrijednost. Ako jest, inferira povratni tip kao R
i vraća ga. Inače, vraća any
.
2. Inferiranje tipa elementa polja
Još jedan koristan scenarij je izdvajanje tipa elementa iz polja:
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
Ovdje, ArrayElementType<T>
provjerava je li T
tip polja. Ako jest, inferira tip elementa kao U
i vraća ga. Ako nije, vraća never
.
Napredni primjeri upotrebe infer
1. Inferiranje parametara konstruktora
Možete koristiti infer
za izdvajanje tipova parametara konstruktorske funkcije:
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]
U ovom slučaju, ConstructorParameters<T>
uzima tip konstruktorske funkcije T
. Inferira tipove parametara konstruktora kao P
i vraća ih kao tuple.
2. Izdvajanje svojstava iz objektnih tipova
infer
se također može koristiti za izdvajanje specifičnih svojstava iz objektnih tipova pomoću mapiranih i uvjetnih tipova:
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; }
//Sučelje koje predstavlja geografske koordinate.
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; }
Ovdje, PickByType<T, K, U>
stvara novi tip koji uključuje samo ona svojstva od T
(s ključevima u K
) čije su vrijednosti dodjeljive tipu U
. Mapirani tip iterira preko ključeva od T
, a uvjetni tip filtrira ključeve koji ne odgovaraju navedenom tipu.
3. Rad s Promise objektima
Možete inferirati riješeni (resolved) tip Promise
objekta:
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[]
Tip Awaited<T>
uzima tip T
, za koji se očekuje da je Promise. Tip zatim inferira riješeni tip U
Promise objekta i vraća ga. Ako T
nije Promise, vraća T. Ovo je ugrađeni pomoćni tip u novijim verzijama TypeScripta.
4. Izdvajanje tipa iz polja Promise objekata
Kombiniranje Awaited
i inferiranja tipa polja omogućuje vam da inferirate tip koji rješava polje Promise objekata. Ovo je posebno korisno kod rada s 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]
Ovaj primjer prvo definira dvije asinkrone funkcije, getUSDRate
i getEURRate
, koje simuliraju dohvaćanje tečajeva. Pomoćni tip PromiseArrayReturnType
zatim izdvaja riješeni tip iz svakog Promise
objekta u polju, što rezultira tuple tipom gdje je svaki element awaited tip odgovarajućeg Promise objekta.
Praktični primjeri u različitim domenama
1. Aplikacija za e-trgovinu
Uzmimo u obzir aplikaciju za e-trgovinu gdje dohvaćate detalje proizvoda s API-ja. Možete koristiti infer
za izdvajanje tipa podataka o proizvodu:
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> {
// Simulacija API poziva
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);
U ovom primjeru definiramo sučelje Product
i funkciju fetchProduct
koja dohvaća detalje proizvoda s API-ja. Koristimo Awaited
i ReturnType
za izdvajanje tipa Product
iz povratnog tipa funkcije fetchProduct
, što nam omogućuje provjeru tipova u funkciji displayProductDetails
.
2. Internazionalizacija (i18n)
Pretpostavimo da imate funkciju za prevođenje koja vraća različite stringove ovisno o lokalizaciji. Možete koristiti infer
za izdvajanje povratnog tipa ove funkcije radi sigurnosti tipova:
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'); // Izlaz: Bienvenue, Jean!
Ovdje se TranslationType
inferira kao sučelje Translations
, osiguravajući da funkcija greetUser
ima točne informacije o tipu za pristup prevedenim stringovima.
3. Rukovanje API odgovorima
Prilikom rada s API-jima, struktura odgovora može biti složena. infer
može pomoći u izdvajanju specifičnih tipova podataka iz ugniježđenih API odgovora:
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>> {
// Simulacija API poziva
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);
}
});
U ovom primjeru definiramo sučelje ApiResponse
i sučelje UserData
. Koristimo infer
i indeksiranje tipova za izdvajanje UserProfileType
iz API odgovora, osiguravajući da funkcija displayUserProfile
prima ispravan tip.
Najbolje prakse za korištenje infer
- Neka bude jednostavno: Koristite
infer
samo kada je to nužno. Prekomjerna upotreba može učiniti vaš kod težim za čitanje i razumijevanje. - Dokumentirajte svoje tipove: Dodajte komentare kako biste objasnili što rade vaši uvjetni tipovi i
infer
izrazi. - Testirajte svoje tipove: Koristite TypeScriptovu provjeru tipova kako biste osigurali da se vaši tipovi ponašaju kako se očekuje.
- Uzmite u obzir performanse: Složeni uvjetni tipovi ponekad mogu utjecati na vrijeme kompilacije. Budite svjesni složenosti svojih tipova.
- Koristite pomoćne tipove: TypeScript pruža nekoliko ugrađenih pomoćnih tipova (npr.
ReturnType
,Awaited
) koji mogu pojednostaviti vaš kod i smanjiti potrebu za prilagođeniminfer
izrazima.
Česte zamke
- Netočna inferencija: Ponekad TypeScript može inferirati tip koji nije onaj koji očekujete. Dvaput provjerite definicije tipova i uvjete.
- Cikličke ovisnosti: Budite oprezni pri definiranju rekurzivnih tipova pomoću
infer
, jer to može dovesti do cikličkih ovisnosti i grešaka pri kompilaciji. - Previše složeni tipovi: Izbjegavajte stvaranje previše složenih uvjetnih tipova koje je teško razumjeti i održavati. Razbijte ih na manje, upravljivije tipove.
Alternative za infer
Iako je infer
moćan alat, postoje situacije u kojima bi alternativni pristupi mogli biti prikladniji:
- Tvrdnje o tipu (Type Assertions): U nekim slučajevima možete koristiti tvrdnje o tipu kako biste eksplicitno naveli tip vrijednosti umjesto da ga inferirate. Međutim, budite oprezni s tvrdnjama o tipu jer mogu zaobići provjeru tipova.
- Čuvari tipa (Type Guards): Čuvari tipa mogu se koristiti za sužavanje tipa vrijednosti na temelju provjera u vremenu izvođenja. Ovo je korisno kada trebate rukovati različitim tipovima ovisno o uvjetima u vremenu izvođenja.
- Pomoćni tipovi: TypeScript pruža bogat skup pomoćnih tipova koji mogu obraditi mnoge uobičajene zadatke manipulacije tipovima bez potrebe za prilagođenim
infer
izrazima.
Zaključak
Ključna riječ infer
u TypeScriptu, kada se kombinira s uvjetnim tipovima, otključava napredne mogućnosti manipulacije tipovima. Omogućuje vam izdvajanje specifičnih tipova iz složenih struktura tipova, što vam omogućuje pisanje robusnijeg, održivijeg i tipski sigurnijeg koda. Od inferiranja povratnih tipova funkcija do izdvajanja svojstava iz objektnih tipova, mogućnosti su ogromne. Razumijevanjem principa i najboljih praksi navedenih u ovom vodiču, možete iskoristiti infer
do punog potencijala i podići svoje TypeScript vještine. Ne zaboravite dokumentirati svoje tipove, temeljito ih testirati i razmotriti alternativne pristupe kada je to prikladno. Ovladavanje ključnom riječi infer
osnažuje vas za pisanje doista izražajnog i moćnog TypeScript koda, što u konačnici dovodi do boljeg softvera.