Norsk

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

Vanlige fallgruver

Alternativer til infer

Selv om infer er et kraftig verktøy, finnes det situasjoner der alternative metoder kan være mer passende:

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.