Entfesseln Sie die Leistungsfähigkeit von TypeScript mit erweiterten Conditional und Mapped Types. Erstellen Sie flexible, typsichere Anwendungen.
Erweiterte TypeScript-Muster: Meisterschaft in Conditional und Mapped Types
Die Stärke von TypeScript liegt in seiner Fähigkeit, eine starke Typisierung bereitzustellen, wodurch Sie Fehler frühzeitig erkennen und besser wartbaren Code schreiben können. Während Basistypen wie string
, number
und boolean
grundlegend sind, erschließen erweiterte Funktionen von TypeScript, wie z. B. Conditional und Mapped Types, eine neue Dimension der Flexibilität und Typsicherheit. Dieser umfassende Leitfaden befasst sich mit diesen leistungsstarken Konzepten und stattet Sie mit dem Wissen aus, um wirklich dynamische und anpassungsfähige TypeScript-Anwendungen zu erstellen.
Was sind Conditional Types?
Conditional Types ermöglichen es Ihnen, Typen zu definieren, die von einer Bedingung abhängen, ähnlich wie ein ternärer Operator in JavaScript (condition ? trueValue : falseValue
). Sie ermöglichen es Ihnen, komplexe Typbeziehungen auszudrücken, basierend darauf, ob ein Typ eine bestimmte Einschränkung erfüllt.
Syntax
Die grundlegende Syntax für einen Conditional Type lautet:
T extends U ? X : Y
T
: Der zu überprüfende Typ.U
: Der Typ, gegen den geprüft werden soll.extends
: Das Schlüsselwort, das eine Subtypbeziehung angibt.X
: Der Typ, der verwendet werden soll, wennT
zuU
zuweisbar ist.Y
: Der Typ, der verwendet werden soll, wennT
nicht zuU
zuweisbar ist.
Im Wesentlichen gilt: Wenn T extends U
als wahr ausgewertet wird, wird der Typ zu X
aufgelöst; andernfalls wird er zu Y
aufgelöst.
Praktische Beispiele
1. Bestimmen des Typs eines Funktionsparameters
Angenommen, Sie möchten einen Typ erstellen, der bestimmt, ob ein Funktionsparameter eine Zeichenfolge oder eine Zahl ist:
type ParamType<T> = T extends string ? string : number;
function processValue(value: ParamType<string | number>): void {
if (typeof value === "string") {
console.log("Wert ist eine Zeichenfolge:", value);
} else {
console.log("Wert ist eine Zahl:", value);
}
}
processValue("hallo"); // Ausgabe: Wert ist eine Zeichenfolge: hallo
processValue(123); // Ausgabe: Wert ist eine Zahl: 123
In diesem Beispiel ist ParamType<T>
ein Conditional Type. Wenn T
eine Zeichenfolge ist, wird der Typ zu string
aufgelöst; andernfalls wird er zu number
aufgelöst. Die Funktion processValue
akzeptiert entweder eine Zeichenfolge oder eine Zahl, basierend auf diesem Conditional Type.
2. Extrahieren des Rückgabetyps basierend auf dem Eingabetyp
Stellen Sie sich ein Szenario vor, in dem Sie eine Funktion haben, die je nach Eingabe unterschiedliche Typen zurückgibt. Conditional Types können Ihnen helfen, den richtigen Rückgabetyp zu definieren:
interface StringProcessor {
process(input: string): number;
}
interface NumberProcessor {
process(input: number): string;
}
type Processor<T> = T extends string ? StringProcessor : NumberProcessor;
function createProcessor<T extends string | number>(input: T): Processor<T> {
if (typeof input === "string") {
return { process: (input: string) => input.length } as Processor<T>;
} else {
return { process: (input: number) => input.toString() } as Processor<T>;
}
}
const stringProcessor = createProcessor("example");
const numberProcessor = createProcessor(42);
console.log(stringProcessor.process("example")); // Ausgabe: 7
console.log(numberProcessor.process(42)); // Ausgabe: "42"
Hier wählt der Typ Processor<T>
bedingt entweder StringProcessor
oder NumberProcessor
basierend auf dem Typ der Eingabe aus. Dadurch wird sichergestellt, dass die Funktion createProcessor
den richtigen Typ des Prozessorobjekts zurückgibt.
3. Diskriminierte Unions
Conditional Types sind äußerst leistungsfähig, wenn Sie mit diskriminierten Unions arbeiten. Eine diskriminierte Union ist ein Union-Typ, bei dem jedes Mitglied eine gemeinsame, Singleton-Typeigenschaft (die Diskriminante) hat. Dies ermöglicht es Ihnen, den Typ basierend auf dem Wert dieser Eigenschaft einzugrenzen.
interface Square {
kind: "square";
size: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Circle;
type Area<T extends Shape> = T extends { kind: "square" } ? number : string;
function calculateArea(shape: Shape): Area<typeof shape> {
if (shape.kind === "square") {
return shape.size * shape.size;
} else {
return Math.PI * shape.radius * shape.radius;
}
}
const mySquare: Square = { kind: "square", size: 5 };
const myCircle: Circle = { kind: "circle", radius: 3 };
console.log(calculateArea(mySquare)); // Ausgabe: 25
console.log(calculateArea(myCircle)); // Ausgabe: 28.274333882308138
In diesem Beispiel ist der Typ Shape
eine diskriminierte Union. Der Typ Area<T>
verwendet einen Conditional Type, um zu bestimmen, ob die Form ein Quadrat oder ein Kreis ist, und gibt number
für Quadrate und string
für Kreise zurück (obwohl Sie in einem realen Szenario wahrscheinlich konsistente Rückgabetypen wünschen würden, zeigt dies das Prinzip).
Wichtige Erkenntnisse zu Conditional Types
- Ermöglichen die Definition von Typen basierend auf Bedingungen.
- Verbessern die Typsicherheit durch das Ausdrücken komplexer Typbeziehungen.
- Sind nützlich für die Arbeit mit Funktionsparametern, Rückgabetypen und diskriminierten Unions.
Was sind Mapped Types?
Mapped Types bieten eine Möglichkeit, bestehende Typen zu transformieren, indem sie über ihre Eigenschaften abgebildet werden. Sie ermöglichen es Ihnen, neue Typen basierend auf den Eigenschaften eines anderen Typs zu erstellen und Änderungen wie das optionale Festlegen von Eigenschaften, das Hinzufügen von Readonly-Attributen oder das Ändern ihrer Typen anzuwenden.
Syntax
Die allgemeine Syntax für einen Mapped Type lautet:
type NewType<T> = {
[K in keyof T]: ModifiedType;
};
T
: Der Eingabetyp.keyof T
: Ein Typoperator, der eine Union aller Eigenschaftsschlüssel inT
zurückgibt.K in keyof T
: Iteriert über jeden Schlüssel inkeyof T
und weist jede Schlüssel der TypvariablenK
zu.ModifiedType
: Der Typ, auf den jede Eigenschaft abgebildet wird. Dies kann Conditional Types oder andere Typentransformationen umfassen.
Praktische Beispiele
1. Optionale Eigenschaften erstellen
Sie können einen Mapped Type verwenden, um alle Eigenschaften eines vorhandenen Typs optional zu machen:
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = {
[K in keyof User]?: User[K];
};
const partialUser: PartialUser = {
name: "John Doe",
}; // Gültig, da 'id' und 'email' optional sind
Hier ist PartialUser
ein Mapped Type, der über die Schlüssel der User
-Schnittstelle iteriert. Für jeden Schlüssel K
macht er die Eigenschaft optional, indem er den Modifier ?
hinzufügt. User[K]
ruft den Typ der Eigenschaft K
aus der User
-Schnittstelle ab.
2. Eigenschaften schreibgeschützt machen
Ebenso können Sie alle Eigenschaften eines vorhandenen Typs schreibgeschützt machen:
interface Product {
id: number;
name: string;
price: number;
}
type ReadonlyProduct = {
readonly [K in keyof Product]: Product[K];
};
const readonlyProduct: ReadonlyProduct = {
id: 123,
name: "Beispielprodukt",
price: 25.00,
};
// readonlyProduct.price = 30.00; // Fehler: Kann 'price' nicht zuweisen, da es sich um eine schreibgeschützte Eigenschaft handelt.
In diesem Fall ist ReadonlyProduct
ein Mapped Type, der den Modifier readonly
zu jeder Eigenschaft der Product
-Schnittstelle hinzufügt.
3. Transformation von Eigenschaftstypen
Mapped Types können auch verwendet werden, um die Typen von Eigenschaften zu transformieren. Beispielsweise können Sie einen Typ erstellen, der alle String-Eigenschaften in Zahlen umwandelt:
interface Config {
apiUrl: string;
timeout: string;
maxRetries: number;
}
type NumericConfig = {
[K in keyof Config]: Config[K] extends string ? number : Config[K];
};
const numericConfig: NumericConfig = {
apiUrl: 123, // Muss eine Zahl sein, aufgrund der Zuordnung
timeout: 456, // Muss eine Zahl sein, aufgrund der Zuordnung
maxRetries: 3,
};
Dieses Beispiel zeigt die Verwendung eines Conditional Type innerhalb eines Mapped Type. Für jede Eigenschaft K
wird geprüft, ob der Typ von Config[K]
eine Zeichenfolge ist. Wenn dies der Fall ist, wird der Typ auf number
abgebildet; andernfalls bleibt er unverändert.
4. Key-Remapping (seit TypeScript 4.1)
TypeScript 4.1 hat die Möglichkeit eingeführt, Schlüssel innerhalb von Mapped Types mit dem Schlüsselwort as
neu zuzuordnen. Dies ermöglicht es Ihnen, neue Typen mit unterschiedlichen Eigenschaftsnamen basierend auf dem ursprünglichen Typ zu erstellen.
interface Event {
eventId: string;
eventName: string;
eventDate: Date;
}
type TransformedEvent = {
[K in keyof Event as `new${Capitalize<string&K>}`]: Event[K];
};
// Ergebnis:
// {
// newEventId: string;
// newEventName: string;
// newEventDate: Date;
// }
//Capitalize-Funktion zur Großschreibung des ersten Buchstabens verwendet
type Capitalize<S extends string> = Uppercase<string&S> extends never ? string : `$Capitalize<S>`;
//Verwendung mit einem tatsächlichen Objekt
const myEvent: TransformedEvent = {
newEventId: "123",
newEventName: "Neuer Name",
newEventDate: new Date()
};
Hier ordnet der Typ TransformedEvent
jeden Schlüssel K
einem neuen Schlüssel vorangestellt mit "neu" und großgeschrieben neu zu. Die Hilfsfunktion Capitalize
stellt sicher, dass der erste Buchstabe des Schlüssels großgeschrieben wird. Die Schnittmenge string & K
stellt sicher, dass wir nur mit Zeichenfolgeschlüsseln arbeiten und dass wir den richtigen Literaltyp von K erhalten.
Key-Remapping eröffnet leistungsstarke Möglichkeiten zur Transformation und Anpassung von Typen an spezifische Bedürfnisse. Auf diese Weise können Sie Schlüssel basierend auf komplexer Logik umbenennen, filtern oder ändern.
Wichtige Erkenntnisse zu Mapped Types
- Ermöglichen die Transformation bestehender Typen durch Abbildung über deren Eigenschaften.
- Ermöglichen das Festlegen von Eigenschaften als optional, schreibgeschützt oder das Ändern ihrer Typen.
- Sind nützlich für das Erstellen neuer Typen basierend auf den Eigenschaften eines anderen Typs.
- Key-Remapping (eingeführt in TypeScript 4.1) bietet noch mehr Flexibilität bei Typentransformationen.
Kombination von Conditional und Mapped Types
Die wahre Leistungsfähigkeit von Conditional und Mapped Types ergibt sich, wenn Sie sie kombinieren. Auf diese Weise können Sie hochflexible und ausdrucksstarke Typdefinitionen erstellen, die sich an eine Vielzahl von Szenarien anpassen können.
Beispiel: Filtern von Eigenschaften nach Typ
Angenommen, Sie möchten einen Typ erstellen, der die Eigenschaften eines Objekts basierend auf ihrem Typ filtert. Beispielsweise möchten Sie möglicherweise nur die String-Eigenschaften aus einem Objekt extrahieren.
interface Data {
name: string;
age: number;
city: string;
country: string;
isEmployed: boolean;
}
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringData = StringProperties<Data>;
// Ergebnis:
// {
// name: string;
// city: string;
// country: string;
// }
const stringData: StringData = {
name: "John",
city: "New York",
country: "USA",
};
In diesem Beispiel verwendet der Typ StringProperties<T>
einen Mapped Type mit Key-Remapping und einem Conditional Type. Für jede Eigenschaft K
wird geprüft, ob der Typ von T[K]
eine Zeichenfolge ist. Wenn dies der Fall ist, wird der Schlüssel beibehalten; andernfalls wird er auf never
abgebildet, wodurch er effektiv herausgefiltert wird. never
als Mapped Type-Schlüssel entfernt ihn aus dem resultierenden Typ. Dies stellt sicher, dass nur String-Eigenschaften in den Typ StringData
aufgenommen werden.
Utility Types in TypeScript
TypeScript stellt mehrere integrierte Utility Types bereit, die Conditional und Mapped Types nutzen, um gängige Typentransformationen durchzuführen. Das Verständnis dieser Utility Types kann Ihren Code erheblich vereinfachen und die Typsicherheit verbessern.
Häufige Utility Types
Partial<T>
: Macht alle Eigenschaften vonT
optional.Readonly<T>
: Macht alle Eigenschaften vonT
schreibgeschützt.Required<T>
: Macht alle Eigenschaften vonT
erforderlich (entfernt den Modifizierer?
).Pick<T, K extends keyof T>
: Wählt eine Reihe von EigenschaftenK
ausT
aus.Omit<T, K extends keyof T>
: Entfernt eine Reihe von EigenschaftenK
ausT
.Record<K extends keyof any, T>
: Erstellt einen Typ mit einer Reihe von EigenschaftenK
vom TypT
.Exclude<T, U>
: Schließt ausT
alle Typen aus, dieU
zuweisbar sind.Extract<T, U>
: Extrahiert ausT
alle Typen, dieU
zuweisbar sind.NonNullable<T>
: Schließtnull
undundefined
ausT
aus.Parameters<T>
: Erhält die Parameter eines FunktionstypsT
in einem Tupel.ReturnType<T>
: Erhält den Rückgabetyp eines FunktionstypsT
.InstanceType<T>
: Erhält den Instanztyp eines KonstruktorfunktionstypsT
.ThisType<T>
: Dient als Markierung für den kontextbezogenenthis
-Typ.
Diese Utility Types werden mit Conditional und Mapped Types erstellt, was die Leistungsfähigkeit und Flexibilität dieser erweiterten TypeScript-Funktionen zeigt. Beispielsweise wird Partial<T>
wie folgt definiert:
type Partial<T> = {
[P in keyof T]?: T[P];
};
Best Practices für die Verwendung von Conditional und Mapped Types
Obwohl Conditional und Mapped Types leistungsfähig sind, können sie Ihren Code auch komplexer machen, wenn sie nicht sorgfältig verwendet werden. Hier sind einige Best Practices, die Sie beachten sollten:
- Halten Sie es einfach: Vermeiden Sie allzu komplexe Conditional und Mapped Types. Wenn eine Typdefinition zu kompliziert wird, sollten Sie sie in kleinere, besser handhabbare Teile aufteilen.
- Verwenden Sie aussagekräftige Namen: Geben Sie Ihren Conditional und Mapped Types beschreibende Namen, die ihren Zweck eindeutig angeben.
- Dokumentieren Sie Ihre Typen: Fügen Sie Kommentare hinzu, um die Logik hinter Ihren Conditional und Mapped Types zu erläutern, insbesondere wenn sie komplex sind.
- Nutzen Sie Utility Types: Bevor Sie einen benutzerdefinierten Conditional oder Mapped Type erstellen, überprüfen Sie, ob ein integrierter Utility Type dasselbe Ergebnis erzielen kann.
- Testen Sie Ihre Typen: Stellen Sie sicher, dass sich Ihre Conditional und Mapped Types wie erwartet verhalten, indem Sie Unit-Tests schreiben, die verschiedene Szenarien abdecken.
- Berücksichtigen Sie die Leistung: Komplexe Typberechnungen können sich auf die Kompilierungszeiten auswirken. Achten Sie auf die Auswirkungen Ihrer Typdefinitionen auf die Leistung.
Fazit
Conditional und Mapped Types sind wesentliche Werkzeuge für die Beherrschung von TypeScript. Sie ermöglichen es Ihnen, hochflexible, typsichere und wartbare Anwendungen zu erstellen, die sich an komplexe Datenstrukturen und dynamische Anforderungen anpassen. Wenn Sie die in diesem Leitfaden besprochenen Konzepte verstehen und anwenden, können Sie das volle Potenzial von TypeScript freisetzen und robusteren und skalierbaren Code schreiben. Denken Sie daran, mit verschiedenen Kombinationen von Conditional und Mapped Types zu experimentieren, um neue Möglichkeiten zur Lösung anspruchsvoller Typisierungsprobleme zu entdecken, während Sie TypeScript weiter erkunden. Die Möglichkeiten sind wirklich endlos.