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;
};
T
: Der Eingabetyp, den Sie abbilden möchten.K in keyof T
: Iteriert über jeden Schlüssel im EingabetypT
.keyof T
erzeugt eine Union aller Eigenschaftsnamen inT
, undK
repräsentiert jeden einzelnen Schlüssel während der Iteration.Transformation
: Die Transformation, die Sie auf jede Eigenschaft anwenden möchten. Dies könnte das Hinzufügen eines Modifikators (wiereadonly
oder?
), die Änderung des Typs oder etwas völlig anderes sein.
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
T
: Der Typ, der überprüft wird.U
: Der Typ, von demT
abgeleitet wird (die Bedingung).X
: Der Typ, der zurückgegeben wird, wennT
vonU
abgeleitet ist (die Bedingung ist wahr).Y
: Der Typ, der zurückgegeben wird, wennT
nicht vonU
abgeleitet ist (die Bedingung ist falsch).
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.
Partial<T>
: Macht alle Eigenschaften vonT
optional.Required<T>
: Macht alle Eigenschaften vonT
erforderlich.Readonly<T>
: Macht alle Eigenschaften vonT
schreibgeschützt.Pick<T, K>
: Wählt eine Reihe von EigenschaftenK
ausT
aus.Omit<T, K>
: Entfernt eine Reihe von EigenschaftenK
ausT
.Record<K, T>
: Konstruiert einen Typ mit einer Reihe von EigenschaftenK
des TypsT
.Exclude<T, U>
: Schließt ausT
alle Typen aus, dieU
zugewiesen werden können.Extract<T, U>
: Extrahiert ausT
alle Typen, dieU
zugewiesen werden können.NonNullable<T>
: Schließtnull
undundefined
ausT
aus.Parameters<T>
: Ermittelt die Parameter eines FunktionstypsT
.ReturnType<T>
: Ermittelt den Rückgabetyp eines FunktionstypsT
.InstanceType<T>
: Ermittelt den Instanztyp eines Konstruktor-FunktionstypsT
.
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
- Halten Sie es einfach: Obwohl Mapped Types und Conditional Types mächtig sind, können sie Ihren Code auch komplexer machen. Versuchen Sie, Ihre Typ-Transformationen so einfach wie möglich zu halten.
- Verwenden Sie Utility-Typen: Nutzen Sie die integrierten Utility-Typen von TypeScript, wann immer möglich. Sie sind gut getestet und können Ihren Code vereinfachen.
- Dokumentieren Sie Ihre Typen: Dokumentieren Sie Ihre Typ-Transformationen klar und deutlich, besonders wenn sie komplex sind. Dies hilft anderen Entwicklern, Ihren Code zu verstehen.
- Testen Sie Ihre Typen: Verwenden Sie die Typüberprüfung von TypeScript, um sicherzustellen, dass Ihre Typ-Transformationen wie erwartet funktionieren. Sie können Unit-Tests schreiben, um das Verhalten Ihrer Typen zu überprüfen.
- Bedenken Sie die Leistung: Komplexe Typ-Transformationen können die Leistung Ihres TypeScript-Compilers beeinträchtigen. Achten Sie auf die Komplexität Ihrer Typen und vermeiden Sie unnötige Berechnungen.
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.