Svenska

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

Vanliga fallgropar

Alternativ till infer

Medan infer är ett kraftfullt verktyg finns det situationer där alternativa tillvägagångssätt kan vara lämpligare:

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.