Nederlands

Een uitgebreide gids voor het 'infer'-sleutelwoord in TypeScript, met uitleg over het gebruik met conditionele types voor krachtige type-extractie en -manipulatie.

Beheersing van TypeScript Infer: Conditionele Type-extractie voor Geavanceerde Typemanipulatie

Het typesysteem van TypeScript is ongelooflijk krachtig, waardoor ontwikkelaars robuuste en onderhoudbare applicaties kunnen bouwen. Een van de belangrijkste features die deze kracht mogelijk maakt, is het infer-sleutelwoord in combinatie met conditionele types. Deze combinatie biedt een mechanisme voor het extraheren van specifieke types uit complexe typestructuren. Deze blogpost gaat diep in op het infer-sleutelwoord, legt de functionaliteit ervan uit en toont geavanceerde use-cases. We zullen praktische voorbeelden verkennen die van toepassing zijn op diverse softwareontwikkelingsscenario's, van API-interactie tot de manipulatie van complexe datastructuren.

Wat zijn Conditionele Types?

Voordat we dieper ingaan op infer, laten we eerst de conditionele types kort herhalen. Conditionele types in TypeScript stellen u in staat een type te definiëren op basis van een voorwaarde, vergelijkbaar met een ternaire operator in JavaScript. De basissyntaxis is:

T extends U ? X : Y

Dit leest als: "Als type T toewijsbaar is aan type U, dan is het type X; anders is het type Y."

Voorbeeld:

type IsString<T> = T extends string ? true : false;

type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false

Introductie van het infer Sleutelwoord

Het infer-sleutelwoord wordt gebruikt binnen de extends-clausule van een conditioneel type om een typevariabele te declareren die kan worden afgeleid van het type dat wordt gecontroleerd. In essentie stelt het u in staat om een deel van een type te "vangen" voor later gebruik.

Basissyntaxis:

type MyType<T> = T extends (infer U) ? U : never;

In dit voorbeeld, als T toewijsbaar is aan een bepaald type, zal TypeScript proberen het type van U af te leiden. Als de afleiding succesvol is, zal het type U zijn; anders zal het never zijn.

Eenvoudige Voorbeelden van infer

1. Het Afleiden van het Returntype van een Functie

Een veelvoorkomend gebruik is het afleiden van het returntype van een functie:

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

In dit voorbeeld neemt ReturnType<T> een functietype T als input. Het controleert of T toewijsbaar is aan een functie die willekeurige argumenten accepteert en een waarde retourneert. Als dat zo is, leidt het het returntype af als R en retourneert het. Anders retourneert het any.

2. Het Afleiden van het Array-elementtype

Een ander nuttig scenario is het extraheren van het elementtype uit een 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

Hier controleert ArrayElementType<T> of T een array-type is. Als dat zo is, leidt het het elementtype af als U en retourneert het. Zo niet, dan retourneert het never.

Geavanceerde Use Cases van infer

1. Het Afleiden van Parameters van een Constructor

U kunt infer gebruiken om de parametertypes van een constructor-functie te extraheren:

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]

In dit geval neemt ConstructorParameters<T> een constructor-functietype T. Het leidt de types van de constructorparameters af als P en retourneert deze als een tuple.

2. Eigenschappen Extraheren uit Objecttypes

infer kan ook worden gebruikt om specifieke eigenschappen uit objecttypes te extraheren met behulp van mapped types en conditionele types:

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; }

//Een interface die geografische coördinaten voorstelt.
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; }

Hier creëert PickByType<T, K, U> een nieuw type dat alleen de eigenschappen van T bevat (met sleutels in K) waarvan de waarden toewijsbaar zijn aan type U. Het mapped type itereert over de sleutels van T, en het conditionele type filtert de sleutels eruit die niet overeenkomen met het opgegeven type.

3. Werken met Promises

U kunt het opgeloste type van een Promise afleiden:

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[]

Het Awaited<T>-type neemt een type T, dat wordt verwacht een Promise te zijn. Het type leidt vervolgens het opgeloste type U van de Promise af en retourneert dit. Als T geen promise is, retourneert het T. Dit is een ingebouwd utility type in nieuwere versies van TypeScript.

4. Het Type van een Array van Promises Extraheren

Door Awaited te combineren met array-type-inferentie kunt u het type afleiden dat wordt opgelost door een array van Promises. Dit is met name handig bij het omgaan met 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]

Dit voorbeeld definieert eerst twee asynchrone functies, getUSDRate en getEURRate, die het ophalen van wisselkoersen simuleren. Het utility type PromiseArrayReturnType extraheert vervolgens het opgeloste type van elke Promise in de array, wat resulteert in een tuple-type waarbij elk element het awaited type is van de corresponderende Promise.

Praktische Voorbeelden uit Verschillende Domeinen

1. E-commerce Applicatie

Denk aan een e-commerce applicatie waar u productdetails ophaalt van een API. U kunt infer gebruiken om het type van de productdata te extraheren:

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> {
  // Simuleer API-aanroep
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        id: productId,
        name: 'Voorbeeldproduct',
        price: 29.99,
        description: 'Een voorbeeldproduct',
        imageUrl: 'https://example.com/image.jpg',
        category: 'Elektronica',
        rating: 4.5,
        countryOfOrigin: 'Canada'
      });
    }, 500);
  });
}


type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product

function displayProductDetails(product: ProductType) {
  console.log(`Productnaam: ${product.name}`);
  console.log(`Prijs: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}

fetchProduct(123).then(displayProductDetails);

In dit voorbeeld definiëren we een Product-interface en een fetchProduct-functie die productdetails van een API ophaalt. We gebruiken Awaited en ReturnType om het Product-type te extraheren uit het returntype van de fetchProduct-functie, waardoor we de displayProductDetails-functie kunnen typechecken.

2. Internationalisatie (i18n)

Stel dat u een vertaalfunctie heeft die verschillende strings retourneert op basis van de locale. U kunt infer gebruiken om het returntype van deze functie te extraheren voor typeveiligheid:

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!

Hier wordt TranslationType afgeleid als de Translations-interface, wat ervoor zorgt dat de greetUser-functie de juiste type-informatie heeft voor toegang tot vertaalde strings.

3. API Responsafhandeling

Bij het werken met API's kan de responsstructuur complex zijn. infer kan helpen bij het extraheren van specifieke datatypes uit geneste API-responsen:

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>> {
  // Simuleer API-aanroep
  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(`Naam: ${profile.firstName} ${profile.lastName}`);
  console.log(`Land: ${profile.country}`);
}

fetchUser(123).then((response) => {
  if (response.status === 200) {
    displayUserProfile(response.data.profile);
  }
});

In dit voorbeeld definiëren we een ApiResponse-interface en een UserData-interface. We gebruiken infer en type-indexering om UserProfileType uit de API-respons te extraheren, wat ervoor zorgt dat de displayUserProfile-functie het juiste type ontvangt.

Best Practices voor het Gebruik van infer

Veelvoorkomende Valkuilen

Alternatieven voor infer

Hoewel infer een krachtig hulpmiddel is, zijn er situaties waarin alternatieve benaderingen geschikter kunnen zijn:

Conclusie

Het infer-sleutelwoord in TypeScript, in combinatie met conditionele types, ontsluit geavanceerde mogelijkheden voor typemanipulatie. Het stelt u in staat om specifieke types te extraheren uit complexe typestructuren, waardoor u robuustere, onderhoudbare en type-veilige code kunt schrijven. Van het afleiden van functie-returntypes tot het extraheren van eigenschappen uit objecttypes, de mogelijkheden zijn enorm. Door de principes en best practices die in deze gids worden uiteengezet te begrijpen, kunt u infer volledig benutten en uw TypeScript-vaardigheden naar een hoger niveau tillen. Vergeet niet uw types te documenteren, ze grondig te testen en alternatieve benaderingen te overwegen wanneer dat gepast is. Het beheersen van infer stelt u in staat om echt expressieve en krachtige TypeScript-code te schrijven, wat uiteindelijk leidt tot betere software.