En omfattende guide til TypeScript 'infer'-nøkkelordet, som forklarer hvordan det brukes med betingede typer for kraftig typeutvinning og manipulasjon.
Mestre TypeScript Infer: Betinget typeutvinning for avansert typemanipulering
Type-systemet i TypeScript er utrolig kraftig, og lar utviklere skape robuste og vedlikeholdbare applikasjoner. En av nøkkelfunksjonene som muliggjør denne kraften er infer
-nøkkelordet, brukt i kombinasjon med betingede typer. Denne kombinasjonen gir en mekanisme for å trekke ut spesifikke typer fra komplekse typestrukturer. Dette blogginnlegget dykker dypt ned i infer
-nøkkelordet, forklarer funksjonaliteten og viser frem avanserte brukstilfeller. Vi vil utforske praktiske eksempler som er anvendelige i ulike programvareutviklingsscenarioer, fra API-interaksjon til kompleks manipulasjon av datastrukturer.
Hva er betingede typer?
Før vi dykker ned i infer
, la oss raskt gjennomgå betingede typer. Betingede typer i TypeScript lar deg definere en type basert på en betingelse, likt en ternær operator i JavaScript. Grunnleggund syntaxen er:
T extends U ? X : Y
Dette leses som: "Hvis type T
kan tildeles type 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
Introduserer infer
-nøkkelordet
infer
-nøkkelordet brukes innenfor extends
-klausulen i en betinget type for å deklarere en typevariabel som kan utledes fra typen som sjekkes. I hovedsak lar det deg "fange opp" en del av en type for senere bruk.
Grunnleggende syntax:
type MyType<T> = T extends (infer U) ? U : never;
I dette eksempelet, hvis T
kan tildeles en viss type, vil TypeScript prøve å utlede typen av U
. Hvis utledningen er vellykket, vil typen være U
; ellers vil den være never
.
Enkle eksempler på infer
1. Utledning av returtypen til en funksjon
Et vanlig brukstilfelle er å utlede returtypen til en funksjon:
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 eksempelet tar ReturnType<T>
en funksjonstype T
som input. Den sjekker om T
kan tildeles en funksjon som aksepterer vilkårlige argumenter og returnerer en verdi. Hvis den kan det, utleder den returtypen som R
og returnerer den. Ellers returnerer den any
.
2. Utledning av elementtype fra et array
Et annet nyttig scenario er å trekke ut 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 sjekker ArrayElementType<T>
om T
er en array-type. Hvis den er det, utleder den elementtypen som U
og returnerer den. Hvis ikke, returnerer den never
.
Avanserte brukstilfeller av infer
1. Utledning av parametere til en konstruktør
Du kan bruke infer
til å trekke ut parametertypene til en konstruktørfunksjon:
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 tilfellet tar ConstructorParameters<T>
en konstruktørfunksjonstype T
. Den utleder typene til konstruktørparameterne som P
og returnerer dem som en tuple.
2. Utvinning av egenskaper fra objekttyper
infer
kan også brukes til å trekke ut spesifikke egenskaper fra objekttyper ved hjelp av 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 grensesnitt som representerer 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 lager PickByType<T, K, U>
en ny type som kun inkluderer egenskapene til T
(med nøkler i K
) hvis verdier kan tildeles typen U
. Mapped type itererer over nøklene til T
, og den betingede typen filtrerer bort nøklene som ikke samsvarer med den spesifiserte typen.
3. Arbeide med Promises
Du kan utlede den resolverte typen til 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 tar en type T
, som forventes å være en Promise. Typen utleder deretter den resolverte typen U
til Promise, og returnerer den. Hvis T
ikke er en promise, returnerer den T. Dette er en innebygd utility-type i nyere versjoner av TypeScript.
4. Utledning av typen til et array av Promises
Kombinasjonen av Awaited
og array-typeutledning lar deg utlede den typen som løses av et array av Promises. Dette er spesielt nyttig når du arbeider 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 eksemplet definerer først to asynkrone funksjoner, getUSDRate
og getEURRate
, som simulerer henting av valutakurser. PromiseArrayReturnType
utility-typen trekker deretter ut den resolverte typen fra hver Promise
i arrayet, noe som resulterer i en tuple-type der hvert element er den avventede typen til den korresponderende Promise.
Praktiske eksempler på tvers av ulike domener
1. E-handelsapplikasjon
Tenk deg en e-handelsapplikasjon der du henter produktdetaljer fra et API. Du kan bruke infer
til å trekke ut typen til produktdataene:
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> {
// Simuler API-kall
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Eksempel Produkt',
price: 29.99,
description: 'Et eksempelprodukt',
imageUrl: 'https://example.com/image.jpg',
category: 'Elektronikk',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Produktnavn: ${product.name}`);
console.log(`Pris: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
I dette eksempelet definerer vi et Product
-grensesnitt og en fetchProduct
-funksjon som henter produktdetaljer fra et API. Vi bruker Awaited
og ReturnType
for å trekke ut Product
-typen fra returtypen til fetchProduct
-funksjonen, noe som lar oss type-sjekke displayProductDetails
-funksjonen.
2. Internasjonalisering (i18n)
Anta at du har en oversettelsesfunksjon som returnerer forskjellige strenger basert på lokalet. Du kan bruke infer
til å trekke ut returtypen til denne funksjonen for typesikkerhet:
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 utledes TranslationType
til å være Translations
-grensesnittet, noe som sikrer at greetUser
-funksjonen har korrekt typeinformasjon for å få tilgang til oversatte strenger.
3. API-respons Håndtering
Når du arbeider med API-er, kan responsstrukturen være kompleks. infer
kan hjelpe med å trekke ut spesifikke datatyper fra nestede API-responser:
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>> {
// Simuler API-kall
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(`Navn: ${profile.firstName} ${profile.lastName}`);
console.log(`Land: ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
I dette eksempelet definerer vi et ApiResponse
-grensesnitt og et UserData
-grensesnitt. Vi bruker infer
og typeindeksering for å trekke ut UserProfileType
fra API-responsen, noe som sikrer at displayUserProfile
-funksjonen mottar riktig type.
Beste praksis for bruk av infer
- Hold det enkelt: Bruk
infer
kun når det er nødvendig. Overforbruk kan gjøre koden din vanskeligere å lese og forstå. - Dokumenter typene dine: Legg til kommentarer for å forklare hva betingede typer og
infer
-setninger gjør. - Test typene dine: Bruk TypeScript's typesjekking for å sikre at typene dine oppfører seg som forventet.
- Vurder ytelse: Komplekse betingede typer kan noen ganger påvirke kompileringstiden. Vær oppmerksom på kompleksiteten i typene dine.
- Bruk Utility Types: TypeScript tilbyr flere innebygde utility-typer (f.eks.
ReturnType
,Awaited
) som kan forenkle koden din og redusere behovet for egendefinerteinfer
-setninger.
Vanlige fallgruver
- Feilaktig utledning: Noen ganger kan TypeScript utlede en type som ikke er det du forventer. Dobbeltsjekk typedefinisjonene og betingelsene dine.
- Sirkulære avhengigheter: Vær forsiktig når du definerer rekursive typer med
infer
, da de kan føre til sirkulære avhengigheter og kompileringsfeil. - Overdrevent komplekse typer: Unngå å lage overdrevent komplekse betingede typer som er vanskelige å forstå og vedlikeholde. Del dem opp i mindre, mer håndterbare typer.
Alternativer til infer
Selv om infer
er et kraftig verktøy, finnes det situasjoner der alternative metoder kan være mer passende:
- Type Assertions (Typepåstander): I noen tilfeller kan du bruke typepåstander for eksplisitt å spesifisere typen til en verdi i stedet for å utlede den. Vær imidlertid forsiktig med typepåstander, da de kan omgå typesjekking.
- Type Guards: Type guards kan brukes til å begrense typen til en verdi basert på kjøretidssjekker. Dette er nyttig når du trenger å håndtere forskjellige typer basert på kjøretidsbetingelser.
- Utility Types: TypeScript tilbyr et rikt sett med utility-typer som kan håndtere mange vanlige typemanipuleringsoppgaver uten behov for egendefinerte
infer
-setninger.
Konklusjon
infer
-nøkkelordet i TypeScript, når det kombineres med betingede typer, låser opp avanserte typemanipuleringsmuligheter. Det lar deg trekke ut spesifikke typer fra komplekse typestrukturer, slik at du kan skrive mer robust, vedlikeholdbar og typesikker kode. Fra å utlede funksjonelle retultyper til å trekke ut egenskaper fra objekttyper, er mulighetene enorme. Ved å forstå prinsippene og beste praksisene som er skissert i denne guiden, kan du utnytte infer
til sitt fulle potensial og heve TypeScript-ferdighetene dine. Husk å dokumentere typene dine, teste dem grundig, og vurdere alternative tilnærminger når det er hensiktsmessig. Å mestre infer
gir deg muligheten til å skrive genuint uttrykksfull og kraftig TypeScript-kode, noe som til syvende og sist fører til bedre programvare.