TypeScript Generics im Alltag: Ein praktischer Leitfaden
Du schreibst eine Funktion, die mit jedem Typ funktioniert, aber du willst nicht die Typsicherheit verlieren. Du könntest any verwenden, aber das untergräbt den Zweck von TypeScript. Generics ermöglichen es dir, Code zu schreiben, der mit mehreren Typen funktioniert und dabei die Typinformationen beibehält. Dieser Leitfaden zeigt dir, wie du Generics im Alltag einsetzt, ohne die Dinge zu verkomplizieren.
Was sind Generics?
Generics sind eine Möglichkeit, wiederverwendbare Komponenten zu erstellen, die mit einer Vielzahl von Typen statt nur einem einzigen funktionieren. Sie erlauben es dir, den Typ der Eingabe zu erfassen und in der Ausgabe zu verwenden, wodurch Typsicherheit ohne Einbußen bei der Flexibilität gewährleistet wird.
Betrachte Generics als Typparameter. So wie eine Funktion Wertparameter annimmt, nimmt eine generische Funktion Typparameter an. Zum Beispiel die Identitätsfunktion:
function identity<T>(arg: T): T {
return arg;
}
Hier ist T ein Typparameter. Wenn du identity<string>("hello") aufrufst, leitet TypeScript ab, dass T string ist, und der Rückgabetyp ist string. Wenn du eine Zahl übergibst, gibt es eine Zahl zurück. Das ist präziser als die Verwendung von any.
Warum nicht einfach any verwenden?
Die Verwendung von any deaktiviert die Typprüfung. Du verlierst Autovervollständigung, Refactoring-Unterstützung und der Compiler kann Fehler nicht erkennen. Generics halten die Typinformationen durch deinen Code fließend, sodass du sowohl Flexibilität als auch Sicherheit erhältst.
Generische Funktionen in der Praxis
Schauen wir uns ein häufiges Szenario an: eine Funktion, die das erste Element eines Arrays zurückgibt.
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
Ohne Generics könntest du function firstElement(arr: any[]): any schreiben, was den Typ verliert. Mit Generics, wenn du ein string[] übergibst, ist der Rückgabetyp string | undefined.
Du kannst auch mehrere Typparameter verwenden. Zum Beispiel eine Funktion, die ein Array mithilfe eines Callbacks auf ein neues Array abbildet:
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
Das ist im Wesentlichen, wie Array.prototype.map in der TypeScript-Standardbibliothek typisiert ist.
Generische Interfaces und Typaliase
Generics sind nicht auf Funktionen beschränkt. Du kannst generische Interfaces und Typaliase erstellen, um wiederverwendbare Datenstrukturen zu definieren.
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
Das ist nützlich für API-Antworten, State-Management oder jeden Containertyp.
Du kannst auch Standardtypen angeben:
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
Jetzt wird ApiResponse ohne Typargument standardmäßig auf unknown gesetzt.
Generische Constraints
Manchmal möchtest du die Typen einschränken, die mit einem Generic verwendet werden können. Du kannst das extends-Schlüsselwort verwenden, um eine Einschränkung hinzuzufügen.
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
Jetzt muss T eine length-Eigenschaft haben. Du kannst getLength("hello") oder getLength([1,2,3]) aufrufen, aber nicht getLength(42).
Constraints sind leistungsstark, wenn du mit Objekten arbeitest, die eine gemeinsame Struktur teilen.
Generics mit Klassen verwenden
Klassen können ebenfalls generisch sein. Ein häufiges Beispiel ist eine generische Sammlung:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
const popped = stack.pop(); // number | undefined
Dies stellt sicher, dass nur Zahlen auf einen Zahlen-Stack gelegt werden können.
Praxisbeispiel: Ein typsicherer Event-Emitter
Lass uns einen einfachen Event-Emitter bauen, der Generics verwendet, um Event-Namen auf Payload-Typen abzubilden.
type EventMap = Record<string, any>;
class EventEmitter<T extends EventMap> {
private listeners: { [K in keyof T]?: Array<(payload: T[K]) => void> } = {};
on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends keyof T>(event: K, payload: T[K]): void {
this.listeners[event]?.forEach(listener => listener(payload));
}
}
// Usage
interface MyEvents {
login: { userId: string };
logout: { userId: string };
}
const emitter = new EventEmitter<MyEvents>();
emitter.on('login', (payload) => {
console.log(payload.userId); // string
});
emitter.emit('login', { userId: '123' }); // OK
// emitter.emit('login', { userId: 123 }); // Error
Dieses Muster gibt dir volle Typsicherheit für Event-Namen und Payloads und reduziert Laufzeitfehler.
Häufige Fallstricke und Best Practices
- Übertreibe es nicht mit Generics. Wenn eine Funktion nur mit einem bestimmten Typ funktioniert, mache sie nicht generisch.
- Verwende beschreibende Typparameternamen.
Tist in Ordnung für einfache Fälle, aberTItemoderTResponsekönnen klarer sein. - Bevorzuge Constraints, um Laufzeitfehler zu vermeiden. Wenn du eine Eigenschaft benötigst, füge eine Einschränkung hinzu.
- Nutze Typinferenz. Oft musst du keine Typargumente explizit übergeben; TypeScript kann sie ableiten.
Vergleich: Generics vs. any vs. Union-Typen
| Ansatz | Typsicherheit | Flexibilität | Anwendungsfall |
|---|---|---|---|
| Generics | Hoch | Hoch | Wiederverwendbare Komponenten mit beibehaltenen Typen |
any |
Keine | Hoch | Schnelles Prototyping, Migration von JS |
| Union-Typen | Mittel | Mittel | Begrenzte Menge bekannter Typen |
Generics bieten eine Balance zwischen Flexibilität und Sicherheit und sind daher ideal für Bibliotheken und gemeinsame Utilities.
FAQ
Wann sollte ich Generics anstelle von Union-Typen verwenden?
Verwende Generics, wenn die Typbeziehung zwischen Ein- und Ausgaben wichtig ist. Union-Typen sind für Fälle, in denen ein Wert einer von einer festen Menge von Typen sein kann, aber der spezifische Typ nicht über Operationen hinweg beibehalten werden muss.
Kann ich Generics mit Arrow-Funktionen verwenden?
Ja, aber die Syntax kann in .tsx-Dateien knifflig sein. Du kannst const identity = <T,>(arg: T): T => arg; mit einem nachgestellten Komma schreiben, um JSX-Mehrdeutigkeit zu vermeiden.
Wie beschränke ich ein Generic darauf, nur Objekte zu akzeptieren?
Verwende T extends object oder T extends Record<string, unknown>, um sicherzustellen, dass T ein Objekttyp ist.
Generics sind ein Eckpfeiler des TypeScript-Typsystems. Indem du sie in deinem Alltagscode verwendest, kannst du Funktionen, Interfaces und Klassen schreiben, die sowohl flexibel als auch sicher sind. Beginne mit einfachen generischen Funktionen und integriere dann nach und nach Constraints und komplexere Muster, wie benötigt. Für weitere Entwickler-Tools schau dir unseren JSON Formatter an, um deine JSON-Daten schnell zu validieren und zu formatieren.