EMZETT.
Login

Kurz: 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.

Teil des Kurses TypeScript

Kapitel 8 von 15 im Kurs TypeScript. Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

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 verloren

Generische 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]

Ausgabe:

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]));

Ausgabe:

Baum! [ 1, 2, 3 ]
function laengster<T extends { length: number }>(a: T, b: T): T { return a; }
laengster(5, 10);

Fehlermeldung:

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);

Ausgabe:

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);

Ausgabe:

Buch { links: 1, rechts: 2 } { links: 'x', rechts: true }
Mia

Generische 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());

Ausgabe:

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));

Ausgabe:

[ 1, 2 ]

Wann Generics, wann nicht?

  • Nutze sie, wenn Eingabe- und Ausgabetypen zusammenhängen (T rein, T raus)
  • Wenn T nur einmal vorkommt, brauchst du meist keinen Typ-Parameter
  • Halte die Anzahl klein (ein bis zwei Typ-Parameter) und nenne sie sinnvoll (T, K, V, oder TItem)
  • 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 mit K extends keyof T
  • Auch Interfaces, Typen und Klassen können generisch sein
  • Nur dort einsetzen, wo Typen zusammenhängen

Übungsaufgabe

Schreibe eine generische Funktion gruppiere<T, K extends string>(liste: T[], schluessel: (x: T) => K): Record<K, T[]>.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Klassen in TypeScript

Weiter: Utility-Typen und Typoperatoren

Alle Kapitel: TypeScript im Überblick