En omfattende guide til TypeScript 'infer' nøgleordet, der forklarer, hvordan man bruger det med betingede typer til kraftfuld typeudtrækning og -manipulation.
Mestring af TypeScript Infer: Betinget Typeudtrækning for Avanceret Typemanipulation
TypeScripts typesystem er utroligt kraftfuldt og giver udviklere mulighed for at skabe robuste og vedligeholdelsesvenlige applikationer. En af de centrale funktioner, der muliggør denne styrke, er infer
-nøgleordet, der bruges sammen med betingede typer. Denne kombination giver en mekanisme til at udtrække specifikke typer fra komplekse typestrukturer. Dette blogindlæg dykker dybt ned i infer
-nøgleordet, forklarer dets funktionalitet og viser avancerede anvendelsestilfælde. Vi vil udforske praktiske eksempler, der kan anvendes i forskellige softwareudviklingsscenarier, fra API-interaktion til kompleks datastrukturmanipulation.
Hvad er Betingede Typer?
Før vi dykker ned i infer
, lad os hurtigt gennemgå betingede typer. Betingede typer i TypeScript giver dig mulighed for at definere en type baseret på en betingelse, ligesom en ternær operator i JavaScript. Den grundlæggende syntaks er:
T extends U ? X : Y
Dette læses som: "Hvis typen T
kan tildeles til typen U
, så er typen X
; ellers er typen Y
."
Eksempel:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Introduktion til infer
-nøgleordet
infer
-nøgleordet bruges inden for extends
-delen af en betinget type til at erklære en typevariabel, der kan udledes (infer) fra den type, der kontrolleres. I bund og grund giver det dig mulighed for at "fange" en del af en type til senere brug.
Grundlæggende Syntaks:
type MyType<T> = T extends (infer U) ? U : never;
I dette eksempel, hvis T
kan tildeles en eller anden type, vil TypeScript forsøge at udlede typen af U
. Hvis udledningen lykkes, vil typen være U
; ellers vil den være never
.
Simple Eksempler på infer
1. Udledning af Returtypen fra en Funktion
Et almindeligt anvendelsestilfælde er at udlede returtypen fra en funktion:
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
I dette eksempel tager ReturnType<T>
en funktionstype T
som input. Den kontrollerer, om T
kan tildeles en funktion, der accepterer vilkårlige argumenter og returnerer en værdi. Hvis det er tilfældet, udleder den returtypen som R
og returnerer den. Ellers returnerer den any
.
2. Udledning af Array-elementets Type
Et andet nyttigt scenarie er at udtrække elementtypen fra et array:
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
Her kontrollerer ArrayElementType<T>
, om T
er en array-type. Hvis det er tilfældet, udleder den elementtypen som U
og returnerer den. Hvis ikke, returnerer den never
.
Avancerede Anvendelsestilfælde for infer
1. Udledning af Parametre i en Constructor
Du kan bruge infer
til at udtrække parametertyperne fra en constructor-funktion:
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]
I dette tilfælde tager ConstructorParameters<T>
en constructor-funktionstype T
. Den udleder typerne af constructor-parametrene som P
og returnerer dem som en tuple.
2. Udtrækning af Egenskaber fra Objekttyper
infer
kan også bruges til at udtrække specifikke egenskaber fra objekttyper ved hjælp af mapped types og betingede typer:
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; }
//Et interface, der repræsenterer geografiske koordinater.
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; }
Her opretter PickByType<T, K, U>
en ny type, der kun inkluderer de egenskaber fra T
(med nøgler i K
), hvis værdier kan tildeles typen U
. Mapped type itererer over nøglerne i T
, og den betingede type filtrerer de nøgler fra, der ikke matcher den specificerede type.
3. Arbejde med Promises
Du kan udlede den resolved type af en 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>
-typen tager en type T
, som forventes at være en Promise. Typen udleder derefter den resolved type U
af Promisen og returnerer den. Hvis T
ikke er en promise, returnerer den T. Dette er en indbygget utility type i nyere versioner af TypeScript.
4. Udtrækning af Typen fra et Array af Promises
Ved at kombinere Awaited
og udledning af array-typer kan du udlede den type, der resolves af et array af Promises. Dette er især nyttigt, når man arbejder med 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]
Dette eksempel definerer først to asynkrone funktioner, getUSDRate
og getEURRate
, som simulerer hentning af valutakurser. PromiseArrayReturnType
-utility-typen udtrækker derefter den resolved type fra hver Promise
i arrayet, hvilket resulterer i en tuple-type, hvor hvert element er den awaited type af den tilsvarende Promise.
Praktiske Eksempler på Tværs af Forskellige Domæner
1. E-handelsapplikation
Forestil dig en e-handelsapplikation, hvor du henter produktdetaljer fra et API. Du kan bruge infer
til at udtrække typen af produktdata:
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);
I dette eksempel definerer vi et Product
-interface og en fetchProduct
-funktion, der henter produktdetaljer fra et API. Vi bruger Awaited
og ReturnType
til at udtrække Product
-typen fra fetchProduct
-funktionens returtype, hvilket giver os mulighed for at typechecke displayProductDetails
-funktionen.
2. Internationalisering (i18n)
Antag, at du har en oversættelsesfunktion, der returnerer forskellige strenge baseret på sprogindstillingen (locale). Du kan bruge infer
til at udtrække returtypen fra denne funktion for typesikkerhed:
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!
Her udledes TranslationType
til at være Translations
-interfacet, hvilket sikrer, at greetUser
-funktionen har den korrekte typeinformation til at tilgå oversatte strenge.
3. Håndtering af API-svar
Når man arbejder med API'er, kan svarstrukturen være kompleks. infer
kan hjælpe med at udtrække specifikke datatyper fra indlejrede API-svar:
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);
}
});
I dette eksempel definerer vi et ApiResponse
-interface og et UserData
-interface. Vi bruger infer
og type-indeksering til at udtrække UserProfileType
fra API-svaret, hvilket sikrer, at displayUserProfile
-funktionen modtager den korrekte type.
Bedste Praksis for Brug af infer
- Hold det Simpelt: Brug kun
infer
, når det er nødvendigt. Overdreven brug kan gøre din kode sværere at læse og forstå. - Dokumentér Dine Typer: Tilføj kommentarer for at forklare, hvad dine betingede typer og
infer
-udsagn gør. - Test Dine Typer: Brug TypeScripts typekontrol til at sikre, at dine typer opfører sig som forventet.
- Overvej Ydeevne: Komplekse betingede typer kan undertiden påvirke kompileringstiden. Vær opmærksom på kompleksiteten af dine typer.
- Brug Utility Types: TypeScript tilbyder flere indbyggede utility types (f.eks.
ReturnType
,Awaited
), der kan forenkle din kode og reducere behovet for brugerdefineredeinfer
-udsagn.
Almindelige Faldgruber
- Forkert Udledning: Nogle gange kan TypeScript udlede en type, der ikke er, hvad du forventer. Dobbelttjek dine typedefinitioner og betingelser.
- Cirkulære Afhængigheder: Vær forsigtig, når du definerer rekursive typer med
infer
, da de kan føre til cirkulære afhængigheder og kompileringsfejl. - Overdrevent Komplekse Typer: Undgå at skabe alt for komplekse betingede typer, der er svære at forstå og vedligeholde. Opdel dem i mindre, mere håndterbare typer.
Alternativer til infer
Selvom infer
er et kraftfuldt værktøj, er der situationer, hvor alternative tilgange kan være mere passende:
- Type Assertions: I nogle tilfælde kan du bruge type assertions til eksplicit at specificere typen af en værdi i stedet for at udlede den. Vær dog forsigtig med type assertions, da de kan omgå typekontrol.
- Type Guards: Type guards kan bruges til at indsnævre typen af en værdi baseret på runtime-tjek. Dette er nyttigt, når du skal håndtere forskellige typer baseret på runtime-betingelser.
- Utility Types: TypeScript tilbyder et rigt sæt af utility types, der kan håndtere mange almindelige type-manipulationsopgaver uden behov for brugerdefinerede
infer
-udsagn.
Konklusion
infer
-nøgleordet i TypeScript, når det kombineres med betingede typer, åbner op for avancerede type-manipulationsmuligheder. Det giver dig mulighed for at udtrække specifikke typer fra komplekse typestrukturer, hvilket gør det muligt for dig at skrive mere robust, vedligeholdelsesvenlig og typesikker kode. Fra at udlede funktioners returtyper til at udtrække egenskaber fra objekttyper er mulighederne enorme. Ved at forstå principperne og de bedste praksisser, der er beskrevet i denne guide, kan du udnytte infer
til dets fulde potentiale og løfte dine TypeScript-færdigheder. Husk at dokumentere dine typer, teste dem grundigt og overveje alternative tilgange, når det er relevant. At mestre infer
giver dig mulighed for at skrive virkelig udtryksfuld og kraftfuld TypeScript-kode, hvilket i sidste ende fører til bedre software.