Deutsch

Ein umfassender Leitfaden zu den leistungsstarken Mapped Types und Conditional Types von TypeScript, einschließlich praktischer Beispiele und fortgeschrittener Anwendungsfälle zur Erstellung robuster und typsicherer Anwendungen.

Meistern von TypeScript's Mapped und Conditional Types

TypeScript, ein Superset von JavaScript, bietet leistungsstarke Funktionen zur Erstellung robuster und wartbarer Anwendungen. Unter diesen Funktionen ragen Mapped Types und Conditional Types als wesentliche Werkzeuge für die fortgeschrittene Typmanipulation hervor. Dieser Leitfaden bietet einen umfassenden Überblick über diese Konzepte und untersucht ihre Syntax, praktischen Anwendungen und fortgeschrittenen Anwendungsfälle. Egal, ob Sie ein erfahrener TypeScript-Entwickler sind oder gerade erst Ihre Reise beginnen, dieser Artikel wird Sie mit dem Wissen ausstatten, um diese Funktionen effektiv zu nutzen.

Was sind Mapped Types?

Mapped Types ermöglichen es Ihnen, neue Typen zu erstellen, indem Sie bestehende transformieren. Sie iterieren über die Eigenschaften eines bestehenden Typs und wenden auf jede Eigenschaft eine Transformation an. Dies ist besonders nützlich, um Variationen bestehender Typen zu erstellen, wie zum Beispiel alle Eigenschaften optional oder schreibgeschützt zu machen.

Grundlegende Syntax

Die Syntax für einen Mapped Type lautet wie folgt:

type NewType<T> = {
  [K in keyof T]: Transformation;
};

Praktische Beispiele

Eigenschaften schreibgeschützt machen

Angenommen, Sie haben ein Interface, das ein Benutzerprofil darstellt:

interface UserProfile {
  name: string;
  age: number;
  email: string;
}

Sie können einen neuen Typ erstellen, bei dem alle Eigenschaften schreibgeschützt sind:

type ReadOnlyUserProfile = {
  readonly [K in keyof UserProfile]: UserProfile[K];
};

Jetzt hat ReadOnlyUserProfile die gleichen Eigenschaften wie UserProfile, aber sie sind alle schreibgeschützt.

Eigenschaften optional machen

Ähnlich können Sie alle Eigenschaften optional machen:

type OptionalUserProfile = {
  [K in keyof UserProfile]?: UserProfile[K];
};

OptionalUserProfile hat alle Eigenschaften von UserProfile, aber jede Eigenschaft ist optional.

Eigenschaftstypen ändern

Sie können auch den Typ jeder Eigenschaft ändern. Zum Beispiel können Sie alle Eigenschaften in Strings umwandeln:

type StringifiedUserProfile = {
  [K in keyof UserProfile]: string;
};

In diesem Fall sind alle Eigenschaften in StringifiedUserProfile vom Typ string.

Was sind Conditional Types?

Conditional Types ermöglichen es Ihnen, Typen zu definieren, die von einer Bedingung abhängen. Sie bieten eine Möglichkeit, Typbeziehungen auszudrücken, je nachdem, ob ein Typ eine bestimmte Einschränkung erfüllt. Dies ähnelt einem ternären Operator in JavaScript, aber für Typen.

Grundlegende Syntax

Die Syntax für einen Conditional Type lautet wie folgt:

T extends U ? X : Y

Praktische Beispiele

Bestimmen, ob ein Typ ein String ist

Erstellen wir einen Typ, der string zurückgibt, wenn der Eingabetyp ein String ist, und andernfalls number:

type StringOrNumber<T> = T extends string ? string : number;

type Result1 = StringOrNumber<string>;  // string
type Result2 = StringOrNumber<number>;  // number
type Result3 = StringOrNumber<boolean>; // number

Typ aus einer Union extrahieren

Sie können bedingte Typen verwenden, um einen bestimmten Typ aus einem Union-Typ zu extrahieren. Zum Beispiel, um nicht-nullable Typen zu extrahieren:

type NonNullable<T> = T extends null | undefined ? never : T;

type Result4 = NonNullable<string | null | undefined>; // string

Hier wird der Typ zu never, wenn T null oder undefined ist, was dann durch die Vereinfachung von Union-Typen in TypeScript herausgefiltert wird.

Typen ableiten (Inferring)

Bedingte Typen können auch verwendet werden, um Typen mit dem infer-Schlüsselwort abzuleiten. Dies ermöglicht es Ihnen, einen Typ aus einer komplexeren Typstruktur zu extrahieren.

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function myFunction(x: number): string {
  return x.toString();
}

type Result5 = ReturnType<typeof myFunction>; // string

In diesem Beispiel extrahiert ReturnType den Rückgabetyp einer Funktion. Es prüft, ob T eine Funktion ist, die beliebige Argumente entgegennimmt und einen Typ R zurückgibt. Wenn ja, gibt es R zurück; andernfalls gibt es any zurück.

Kombination von Mapped Types und Conditional Types

Die wahre Stärke von Mapped Types und Conditional Types liegt in ihrer Kombination. Dies ermöglicht es Ihnen, hochflexible und ausdrucksstarke Typ-Transformationen zu erstellen.

Beispiel: Deep Readonly

Ein häufiger Anwendungsfall ist die Erstellung eines Typs, der alle Eigenschaften eines Objekts, einschließlich verschachtelter Eigenschaften, schreibgeschützt macht. Dies kann mit einem rekursiven bedingten Typ erreicht werden.

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
  };
}

type ReadonlyCompany = DeepReadonly<Company>;

Hier wendet DeepReadonly den readonly-Modifikator rekursiv auf alle Eigenschaften und deren verschachtelte Eigenschaften an. Wenn eine Eigenschaft ein Objekt ist, ruft es rekursiv DeepReadonly für dieses Objekt auf. Andernfalls wendet es einfach den readonly-Modifikator auf die Eigenschaft an.

Beispiel: Eigenschaften nach Typ filtern

Angenommen, Sie möchten einen Typ erstellen, der nur Eigenschaften eines bestimmten Typs enthält. Sie können Mapped Types und Conditional Types kombinieren, um dies zu erreichen.

type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Person {
  name: string;
  age: number;
  isEmployed: boolean;
}

type StringProperties = FilterByType<Person, string>; // { name: string; }

type NonStringProperties = Omit<Person, keyof StringProperties>;

In diesem Beispiel iteriert FilterByType über die Eigenschaften von T und prüft, ob der Typ jeder Eigenschaft von U abgeleitet ist. Wenn ja, wird die Eigenschaft in den resultierenden Typ aufgenommen; andernfalls wird sie ausgeschlossen, indem der Schlüssel auf never abgebildet wird. Beachten Sie die Verwendung von „as“, um Schlüssel neu zuzuordnen. Wir verwenden dann `Omit` und `keyof StringProperties`, um die String-Eigenschaften aus dem ursprünglichen Interface zu entfernen.

Fortgeschrittene Anwendungsfälle und Muster

Über die grundlegenden Beispiele hinaus können Mapped Types und Conditional Types in fortgeschritteneren Szenarien verwendet werden, um hochgradig anpassbare und typsichere Anwendungen zu erstellen.

Distributive bedingte Typen

Bedingte Typen sind distributiv, wenn der zu prüfende Typ ein Union-Typ ist. Das bedeutet, dass die Bedingung auf jedes Mitglied der Union einzeln angewendet wird und die Ergebnisse dann zu einem neuen Union-Typ zusammengefasst werden.

type ToArray<T> = T extends any ? T[] : never;

type Result6 = ToArray<string | number>; // string[] | number[]

In diesem Beispiel wird ToArray auf jedes Mitglied der Union string | number einzeln angewendet, was zu string[] | number[] führt. Wäre die Bedingung nicht distributiv, wäre das Ergebnis (string | number)[] gewesen.

Verwendung von Utility-Typen

TypeScript bietet mehrere integrierte Utility-Typen, die Mapped Types und Conditional Types nutzen. Diese Utility-Typen können als Bausteine für komplexere Typ-Transformationen verwendet werden.

Diese Utility-Typen sind leistungsstarke Werkzeuge, die komplexe Typmanipulationen vereinfachen können. Zum Beispiel können Sie Pick und Partial kombinieren, um einen Typ zu erstellen, der nur bestimmte Eigenschaften optional macht:

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}

type OptionalDescriptionProduct = Optional<Product, "description">;

In diesem Beispiel hat OptionalDescriptionProduct alle Eigenschaften von Product, aber die description-Eigenschaft ist optional.

Verwendung von Template-Literal-Typen

Template-Literal-Typen ermöglichen es Ihnen, Typen basierend auf String-Literalen zu erstellen. Sie können in Kombination mit Mapped Types und Conditional Types verwendet werden, um dynamische und ausdrucksstarke Typ-Transformationen zu erstellen. Zum Beispiel können Sie einen Typ erstellen, der allen Eigenschaftsnamen einen bestimmten String voranstellt:

type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${string & K}`]: T[K];
};

interface Settings {
  apiUrl: string;
  timeout: number;
}

type PrefixedSettings = Prefix<Settings, "data_">;

In diesem Beispiel hat PrefixedSettings die Eigenschaften data_apiUrl und data_timeout.

Best Practices und Überlegungen

Fazit

Mapped Types und Conditional Types sind leistungsstarke Funktionen in TypeScript, die es Ihnen ermöglichen, hochflexible und ausdrucksstarke Typ-Transformationen zu erstellen. Durch die Beherrschung dieser Konzepte können Sie die Typsicherheit, Wartbarkeit und Gesamtqualität Ihrer TypeScript-Anwendungen verbessern. Von einfachen Transformationen wie dem optionalen oder schreibgeschützten Machen von Eigenschaften bis hin zu komplexen rekursiven Transformationen und bedingter Logik bieten diese Funktionen die Werkzeuge, die Sie zum Erstellen robuster und skalierbarer Anwendungen benötigen. Erkunden und experimentieren Sie weiter mit diesen Funktionen, um ihr volles Potenzial auszuschöpfen und ein kompetenterer TypeScript-Entwickler zu werden.

Während Sie Ihre TypeScript-Reise fortsetzen, denken Sie daran, die Fülle der verfügbaren Ressourcen zu nutzen, einschließlich der offiziellen TypeScript-Dokumentation, Online-Communitys und Open-Source-Projekten. Machen Sie sich die Macht von Mapped Types und Conditional Types zunutze, und Sie werden gut gerüstet sein, um selbst die schwierigsten typbezogenen Probleme zu bewältigen.