Deutsch

Ein umfassender Leitfaden zum TypeScript 'infer'-Schlüsselwort, der erklärt, wie man es mit bedingten Typen für leistungsstarke Typ-Extraktion und -Manipulation verwendet.

TypeScript Infer meistern: Bedingte Typ-Extraktion für fortgeschrittene Typ-Manipulation

Das Typsystem von TypeScript ist unglaublich leistungsstark und ermöglicht es Entwicklern, robuste und wartbare Anwendungen zu erstellen. Eine der Schlüsselfunktionen, die diese Leistungsfähigkeit ermöglicht, ist das infer-Schlüsselwort in Verbindung mit bedingten Typen. Diese Kombination bietet einen Mechanismus zum Extrahieren spezifischer Typen aus komplexen Typstrukturen. Dieser Blogbeitrag befasst sich eingehend mit dem infer-Schlüsselwort, erklärt seine Funktionalität und zeigt fortgeschrittene Anwendungsfälle. Wir werden praktische Beispiele untersuchen, die auf verschiedene Softwareentwicklungsszenarien anwendbar sind, von der API-Interaktion bis zur Manipulation komplexer Datenstrukturen.

Was sind bedingte Typen?

Bevor wir uns mit infer befassen, wollen wir kurz die bedingten Typen wiederholen. Bedingte Typen in TypeScript ermöglichen es Ihnen, einen Typ basierend auf einer Bedingung zu definieren, ähnlich einem ternären Operator in JavaScript. Die grundlegende Syntax lautet:

T extends U ? X : Y

Das liest sich so: „Wenn der Typ T dem Typ U zugewiesen werden kann, dann ist der Typ X; andernfalls ist der Typ Y.“

Beispiel:

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

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

Einführung des infer-Schlüsselworts

Das infer-Schlüsselwort wird innerhalb der extends-Klausel eines bedingten Typs verwendet, um eine Typvariable zu deklarieren, die aus dem zu prüfenden Typ abgeleitet (inferiert) werden kann. Im Wesentlichen ermöglicht es Ihnen, einen Teil eines Typs für die spätere Verwendung zu „erfassen“.

Grundlegende Syntax:

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

In diesem Beispiel versucht TypeScript, den Typ von U abzuleiten, wenn T einem beliebigen Typ zugewiesen werden kann. Wenn die Ableitung erfolgreich ist, ist der Typ U; andernfalls ist er never.

Einfache Beispiele für infer

1. Ableiten des Rückgabetyps einer Funktion

Ein häufiger Anwendungsfall ist die Ableitung des Rückgabetyps einer 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

In diesem Beispiel nimmt ReturnType<T> einen Funktionstyp T als Eingabe. Es prüft, ob T einer Funktion zugewiesen werden kann, die beliebige Argumente akzeptiert und einen Wert zurückgibt. Wenn ja, leitet es den Rückgabetyp als R ab und gibt ihn zurück. Andernfalls gibt es any zurück.

2. Ableiten des Elementtyps eines Arrays

Ein weiteres nützliches Szenario ist das Extrahieren des Elementtyps aus einem 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 prüft ArrayElementType<T>, ob T ein Array-Typ ist. Wenn ja, leitet es den Elementtyp als U ab und gibt ihn zurück. Wenn nicht, gibt es never zurück.

Fortgeschrittene Anwendungsfälle von infer

1. Ableiten der Parameter eines Konstruktors

Sie können infer verwenden, um die Parametertypen einer Konstruktorfunktion zu extrahieren:

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 diesem Fall nimmt ConstructorParameters<T> einen Konstruktorfunktionstyp T. Es leitet die Typen der Konstruktorparameter als P ab und gibt sie als Tupel zurück.

2. Extrahieren von Eigenschaften aus Objekttypen

infer kann auch verwendet werden, um spezifische Eigenschaften aus Objekttypen mithilfe von gemappten Typen und bedingten Typen zu extrahieren:

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

//Ein Interface, das geografische Koordinaten darstellt.
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 erstellt PickByType<T, K, U> einen neuen Typ, der nur die Eigenschaften von T (mit Schlüsseln in K) enthält, deren Werte dem Typ U zugewiesen werden können. Der gemappte Typ iteriert über die Schlüssel von T, und der bedingte Typ filtert die Schlüssel heraus, die nicht dem angegebenen Typ entsprechen.

3. Arbeiten mit Promises

Sie können den aufgelösten Typ eines Promise ableiten:

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

Der Awaited<T>-Typ nimmt einen Typ T entgegen, von dem erwartet wird, dass er ein Promise ist. Der Typ leitet dann den aufgelösten Typ U des Promise ab und gibt ihn zurück. Wenn T kein Promise ist, gibt er T zurück. Dies ist ein integrierter Utility-Typ in neueren Versionen von TypeScript.

4. Extrahieren des Typs eines Arrays von Promises

Die Kombination von Awaited und der Array-Typ-Ableitung ermöglicht es Ihnen, den Typ abzuleiten, der von einem Array von Promises aufgelöst wird. Dies ist besonders nützlich im Umgang mit 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]

Dieses Beispiel definiert zunächst zwei asynchrone Funktionen, getUSDRate und getEURRate, die das Abrufen von Wechselkursen simulieren. Der Utility-Typ PromiseArrayReturnType extrahiert dann den aufgelösten Typ aus jedem Promise im Array, was zu einem Tupel-Typ führt, bei dem jedes Element der `awaited`-Typ des entsprechenden Promise ist.

Praktische Beispiele aus verschiedenen Bereichen

1. E-Commerce-Anwendung

Stellen Sie sich eine E-Commerce-Anwendung vor, in der Sie Produktdetails von einer API abrufen. Sie können infer verwenden, um den Typ der Produktdaten zu extrahieren:

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> {
  // API-Aufruf simulieren
  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);

In diesem Beispiel definieren wir ein Product-Interface und eine fetchProduct-Funktion, die Produktdetails von einer API abruft. Wir verwenden Awaited und ReturnType, um den Product-Typ aus dem Rückgabetyp der fetchProduct-Funktion zu extrahieren, was uns ermöglicht, die displayProductDetails-Funktion typsicher zu machen.

2. Internationalisierung (i18n)

Angenommen, Sie haben eine Übersetzungsfunktion, die je nach Gebietsschema unterschiedliche Zeichenfolgen zurückgibt. Sie können infer verwenden, um den Rückgabetyp dieser Funktion zur Gewährleistung der Typsicherheit zu extrahieren:

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'); // Ausgabe: Bienvenue, Jean!

Hier wird TranslationType als das Translations-Interface abgeleitet, wodurch sichergestellt wird, dass die greetUser-Funktion die korrekten Typinformationen für den Zugriff auf übersetzte Zeichenfolgen hat.

3. Umgang mit API-Antworten

Bei der Arbeit mit APIs kann die Antwortstruktur komplex sein. infer kann helfen, spezifische Datentypen aus verschachtelten API-Antworten zu extrahieren:

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>> {
  // API-Aufruf simulieren
  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);
  }
});

In diesem Beispiel definieren wir ein ApiResponse-Interface und ein UserData-Interface. Wir verwenden infer und Typ-Indizierung, um den UserProfileType aus der API-Antwort zu extrahieren und sicherzustellen, dass die displayUserProfile-Funktion den korrekten Typ erhält.

Best Practices für die Verwendung von infer

Häufige Fallstricke

Alternativen zu infer

Obwohl infer ein leistungsstarkes Werkzeug ist, gibt es Situationen, in denen alternative Ansätze besser geeignet sein könnten:

Fazit

Das infer-Schlüsselwort in TypeScript schaltet in Kombination mit bedingten Typen fortgeschrittene Fähigkeiten zur Typmanipulation frei. Es ermöglicht Ihnen, spezifische Typen aus komplexen Typstrukturen zu extrahieren, sodass Sie robusteren, wartbareren und typsicheren Code schreiben können. Von der Ableitung von Funktionsrückgabetypen bis zur Extraktion von Eigenschaften aus Objekttypen sind die Möglichkeiten riesig. Indem Sie die in diesem Leitfaden beschriebenen Prinzipien und Best Practices verstehen, können Sie infer voll ausschöpfen und Ihre TypeScript-Fähigkeiten verbessern. Denken Sie daran, Ihre Typen zu dokumentieren, sie gründlich zu testen und bei Bedarf alternative Ansätze in Betracht zu ziehen. Das Meistern von infer befähigt Sie, wirklich ausdrucksstarken und leistungsstarken TypeScript-Code zu schreiben, was letztendlich zu besserer Software führt.