En omfattande guide till TypeScript 'infer'-nyckelordet, som förklarar hur du använder det med villkorliga typer för kraftfull typextraktion och manipulation, inklusive avancerade användningsområden.
Bemästra TypeScript Infer: Villkorlig Typextraktion för Avancerad Typmanipulering
Typsystemet i TypeScript är otroligt kraftfullt, vilket gör att utvecklare kan skapa robusta och underhållbara applikationer. En av nyckelfunktionerna som möjliggör denna kraft är nyckelordet infer
som används i kombination med villkorliga typer. Denna kombination tillhandahåller en mekanism för att extrahera specifika typer från komplexa typstrukturer. Detta blogginlägg fördjupar sig i infer
-nyckelordet, förklarar dess funktionalitet och visar avancerade användningsområden. Vi kommer att utforska praktiska exempel som är tillämpliga på olika programvaruutvecklingsscenarier, från API-interaktion till komplex datastrukturmanipulering.
Vad är villkorliga typer?
Innan vi dyker in i infer
, låt oss snabbt gå igenom villkorliga typer. Villkorliga typer i TypeScript låter dig definiera en typ baserat på ett villkor, liknande en ternär operator i JavaScript. Grundläggande syntaxen är:
T extends U ? X : Y
Detta läses som: "Om typen T
är tilldelningsbar till typen U
, då är typen X
; annars är typen Y
."
Exempel:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Introduktion till infer
-nyckelordet
Nyckelordet infer
används inom extends
-klausulen i en villkorlig typ för att deklarera en typvariabel som kan härledas från den typ som kontrolleras. I huvudsak låter det dig "fånga" en del av en typ för senare användning.
Grundläggande syntax:
type MyType<T> = T extends (infer U) ? U : never;
I detta exempel, om T
är tilldelningsbar till någon typ, kommer TypeScript att försöka härleda typen av U
. Om härledningen lyckas, kommer typen att vara U
; annars kommer den att vara never
.
Enkla exempel på infer
1. Härleda returtypen för en funktion
Ett vanligt användningsfall är att härleda returtypen för 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 `Hej, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
I detta exempel tar ReturnType<T>
en funktionstyp T
som input. Den kontrollerar om T
är tilldelningsbar till en funktion som accepterar vilka argument som helst och returnerar ett värde. Om det är det, härleder den returtypen som R
och returnerar den. Annars returnerar den any
.
2. Härleda elementtyp för en array
Ett annat användbart scenario är att extrahera elementtypen från en 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
Här kontrollerar ArrayElementType<T>
om T
är en arraytyp. Om det är det, härleder den elementtypen som U
och returnerar den. Om inte, returnerar den never
.
Avancerade användningsfall av infer
1. Härleda parametrar för en konstruktor
Du kan använda infer
för att extrahera parametertyperna för en konstruktörsfunktion:
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 det här fallet tar ConstructorParameters<T>
en konstruktörsfunktionstyp T
. Den härleder typerna av konstruktörparametrarna som P
och returnerar dem som en tupel.
2. Extrahera egenskaper från objekttyper
infer
kan också användas för att extrahera specifika egenskaper från objekttyper med hjälp av mappade typer och villkorliga 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; }
//Ett gränssnitt som representerar geografiska 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; }
Här skapar PickByType<T, K, U>
en ny typ som bara inkluderar egenskaperna för T
(med nycklar i K
) vars värden är tilldelningsbara till typen U
. Den mappade typen itererar över nycklarna i T
, och den villkorliga typen filtrerar bort de nycklar som inte matchar den angivna typen.
3. Arbeta med Promises
Du kan härleda den lösta typen av 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[]
Typen Awaited<T>
tar en typ T
, som förväntas vara ett Promise. Typen härleder sedan den lösta typen U
av Promise, och returnerar den. Om T
inte är ett promise, returnerar den T. Detta är en inbyggd hjälptyp i nyare versioner av TypeScript.
4. Extrahera typen av en array av Promises
Att kombinera Awaited
och arraytype-inferens gör att du kan härleda den typ som löses av en array av Promises. Detta är särskilt användbart när man hanterar 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]
Detta exempel definierar först två asynkrona funktioner, getUSDRate
och getEURRate
, som simulerar hämtning av växelkurser. Hjälptypen PromiseArrayReturnType
extraherar sedan den lösta typen från varje Promise
i arrayen, vilket resulterar i en tupeltyp där varje element är den avvaktade typen av motsvarande Promise.
Praktiska exempel inom olika domäner
1. E-handelsapplikation
Tänk dig en e-handelsapplikation där du hämtar produktinformation från ett API. Du kan använda infer
för att extrahera typen av 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> {
// Simulera API-anrop
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Example Product',
price: 29.99,
description: 'En exempelprodukt',
imageUrl: 'https://example.com/image.jpg',
category: 'Elektronik',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Produktnamn: ${product.name}`);
console.log(`Pris: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
I det här exemplet definierar vi ett Product
-gränssnitt och en fetchProduct
-funktion som hämtar produktinformation från ett API. Vi använder Awaited
och ReturnType
för att extrahera Product
-typen från fetchProduct
-funktionens returtyp, vilket gör att vi kan typkontrollera displayProductDetails
-funktionen.
2. Internationalisering (i18n)
Antag att du har en översättningsfunktion som returnerar olika strängar baserat på språket. Du kan använda infer
för att extrahera returtypen för den här funktionen för typsäkerhet:
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!
Här härleds TranslationType
till att vara gränssnittet Translations
, vilket säkerställer att funktionen greetUser
har korrekt typinformation för att komma åt översatta strängar.
3. API-svarsbehandling
När du arbetar med API:er kan svarsstrukturen vara komplex. infer
kan hjälpa till att extrahera specifika datatyper från kapslade 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>> {
// Simulera API-anrop
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(`Namn: ${profile.firstName} ${profile.lastName}`);
console.log(`Land: ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
I det här exemplet definierar vi ett ApiResponse
-gränssnitt och ett UserData
-gränssnitt. Vi använder infer
och typindexering för att extrahera UserProfileType
från API-svaret, vilket säkerställer att funktionen displayUserProfile
får rätt typ.
Bästa praxis för att använda infer
- Håll det enkelt: Använd
infer
endast när det är nödvändigt. Att överanvända det kan göra din kod svårare att läsa och förstå. - Dokumentera dina typer: Lägg till kommentarer för att förklara vad dina villkorliga typer och
infer
-satser gör. - Testa dina typer: Använd TypeScripts typkontroll för att säkerställa att dina typer fungerar som förväntat.
- Överväg prestanda: Komplexa villkorliga typer kan ibland påverka kompileringstiden. Var uppmärksam på komplexiteten i dina typer.
- Använd hjälptyper: TypeScript tillhandahåller flera inbyggda hjälptyper (t.ex.
ReturnType
,Awaited
) som kan förenkla din kod och minska behovet av anpassadeinfer
-satser.
Vanliga fallgropar
- Felaktig inferens: Ibland kan TypeScript härleda en typ som inte är vad du förväntar dig. Dubbelkolla dina typdefinitioner och villkor.
- Cirkulära beroenden: Var försiktig när du definierar rekursiva typer med
infer
, eftersom de kan leda till cirkulära beroenden och kompileringsfel. - Alltför komplexa typer: Undvik att skapa alltför komplexa villkorliga typer som är svåra att förstå och underhålla. Bryt ner dem i mindre, mer hanterbara typer.
Alternativ till infer
Medan infer
är ett kraftfullt verktyg finns det situationer där alternativa tillvägagångssätt kan vara lämpligare:
- Typförsäkringar: I vissa fall kan du använda typförsäkringar för att uttryckligen specificera typen av ett värde istället för att härleda det. Var dock försiktig med typförsäkringar, eftersom de kan kringgå typkontroll.
- Typskydd: Typskydd kan användas för att begränsa typen av ett värde baserat på körningskontroller. Detta är användbart när du behöver hantera olika typer baserat på körningstillstånd.
- Hjälptyper: TypeScript tillhandahåller en rik uppsättning hjälptyper som kan hantera många vanliga typmanipuleringsuppgifter utan behov av anpassade
infer
-satser.
Slutsats
Nyckelordet infer
i TypeScript, när det kombineras med villkorliga typer, låser upp avancerade typmanipuleringsmöjligheter. Det låter dig extrahera specifika typer från komplexa typstrukturer, vilket gör att du kan skriva mer robust, underhållbar och typsäker kod. Från att härleda funktionsreturtyper till att extrahera egenskaper från objekttyper, är möjligheterna enorma. Genom att förstå principerna och bästa praxis som beskrivs i den här guiden kan du utnyttja infer
till sin fulla potential och höja dina TypeScript-kunskaper. Kom ihåg att dokumentera dina typer, testa dem noggrant och överväga alternativa tillvägagångssätt när det är lämpligt. Att bemästra infer
ger dig möjlighet att skriva verkligt uttrycksfull och kraftfull TypeScript-kod, vilket i slutändan leder till bättre programvara.