Generics sind Typ-Parameter: Du schreibst Code einmal und er funktioniert für viele Typen, ohne die Typsicherheit aufzugeben. Ohne Generics müsstest du any nehmen.
Das Problem
function erstesAny(liste: any[]): any {
return liste[0];
}
const x = erstesAny([1, 2, 3]); // x: any, alle Information ist verlorenGenerische Funktionen
function erstes<T>(liste: T[]): T | undefined {
return liste[0];
}
const n = erstes([10, 20]); // n: number | undefined
const s = erstes(["a", "b"]); // s: string | undefined
console.log(n, s, erstes([]));
function tausche<A, B>(paar: [A, B]): [B, A] {
return [paar[1], paar[0]];
}
console.log(tausche(["x", 1])); // [1, "x"] mit Typ [number, string]10 a undefined [ 1, 'x' ]
T steht für "irgendein Typ". TypeScript erkennt ihn beim Aufruf, du musst ihn meist nicht angeben (erstes<number>([1]) geht aber).
Einschränkungen mit extends
Oft soll T bestimmte Eigenschaften haben:
function laengster<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
console.log(laengster("Haus", "Baum!"), laengster([1, 2, 3], [4]));Baum! [ 1, 2, 3 ]
function laengster<T extends { length: number }>(a: T, b: T): T { return a; }
laengster(5, 10);error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.keyof: Schlüssel eines Typs
function hole<T, K extends keyof T>(objekt: T, schluessel: K): T[K] {
return objekt[schluessel];
}
const mia = { name: "Mia", alter: 17 };
const n = hole(mia, "name"); // string
const a = hole(mia, "alter"); // number
console.log(n, a);Mia 17
Ein falscher Schlüssel wie hole(mia, "farbe") wird vom Compiler abgelehnt. T[K] ist der Typ der Eigenschaft.
Generische Typen und Interfaces
interface Kiste<T> {
inhalt: T;
}
type Paar<A, B = A> = { links: A; rechts: B }; // B hat Standard A
const k: Kiste<string> = { inhalt: "Buch" };
const p: Paar<number> = { links: 1, rechts: 2 };
const q: Paar<string, boolean> = { links: "x", rechts: true };
console.log(k.inhalt, p, q);
interface ApiAntwort<T> {
status: number;
daten: T;
fehler?: string;
}
const antwort: ApiAntwort<{ id: number; name: string }[]> = {
status: 200,
daten: [{ id: 1, name: "Mia" }],
};
console.log(antwort.daten[0].name);Buch { links: 1, rechts: 2 } { links: 'x', rechts: true }
MiaGenerische Klassen
class Stapel<T> {
private elemente: T[] = [];
push(x: T): void { this.elemente.push(x); }
pop(): T | undefined { return this.elemente.pop(); }
get groesse(): number { return this.elemente.length; }
}
const s = new Stapel<number>();
s.push(1); s.push(2);
console.log(s.pop(), s.groesse);
class Warteschlange<T> {
private liste: T[] = [];
einreihen(x: T) { this.liste.push(x); }
naechster(): T | undefined { return this.liste.shift(); }
}
const w = new Warteschlange<string>();
w.einreihen("A"); w.einreihen("B");
console.log(w.naechster(), w.naechster(), w.naechster());2 1 A B undefined
Bekannte generische Typen
Du benutzt sie schon: Array<T>, Promise<T>, Map<K, V>, Set<T>, Record<K, V>, Partial<T>.
async function ladeAlle<T>(urls: string[], holen: (u: string) => Promise<T>): Promise<T[]> {
return Promise.all(urls.map(holen));
}
ladeAlle(["a", "bb"], async (u) => u.length).then((r) => console.log(r));[ 1, 2 ]
Wann Generics, wann nicht?
- Nutze sie, wenn Eingabe- und Ausgabetypen zusammenhängen (
Trein,Traus) - Wenn
Tnur einmal vorkommt, brauchst du meist keinen Typ-Parameter - Halte die Anzahl klein (ein bis zwei Typ-Parameter) und nenne sie sinnvoll (
T,K,V, oderTItem) - Erst mit konkreten Typen schreiben, dann verallgemeinern
Merke
- Generics = Typ-Parameter:
function f<T>(x: T): T - TypeScript erkennt den Typ beim Aufruf meist selbst
- Einschränken mit
T extends ...; Schlüssel mitK extends keyof T - Auch Interfaces, Typen und Klassen können generisch sein
- Nur dort einsetzen, wo Typen zusammenhängen
Aufgabe
Schreibe eine generische Funktion gruppiere<T, K extends string>(liste: T[], schluessel: (x: T) => K): Record<K, T[]>.