Dansk

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

Almindelige Faldgruber

Alternativer til infer

Selvom infer er et kraftfuldt værktøj, er der situationer, hvor alternative tilgange kan være mere passende:

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.