Menu

TypeScript Generics: generische Funktionen, Typen und Klassen

Mit Generics arbeitet eine Funktion, ein Interface, ein Typ oder eine Klasse mit vielen Typen und hält sie dabei verbunden: Was hineingeht, bestimmt, was herauskommt. Generische Funktionen, Inferenz von Typargumenten, mehrere Typparameter, generische Interfaces und Klassen, Standardwerte und wann man Generics besser lässt.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Generics sind Typparameter. Eine generische Funktion deklariert einen Platzhaltertyp, meist T genannt, und jeder Aufruf füllt ihn, sodass die Typen, die hineingehen, die Typen bestimmen, die herauskommen. Eine einzige Funktion funktioniert dann für Zahlen, Strings und deine eigenen Objekte, und jeder Aufruf ist trotzdem vollständig typisiert.

<T> hinter dem Funktionsnamen deklariert den Typparameter. items: T[] verwendet ihn für die Eingabe und T | undefined für die Ausgabe, was beide verbindet. Du hast nie number oder string geschrieben: TypeScript hat T aus dem Argument abgeleitet.

Warum nicht any oder unknown?

Ohne Generics würdest du den Parameter als any oder unknown typisieren. Beide akzeptieren jedes Array, und beide verlieren die Verbindung zwischen Eingabe und Ausgabe:

ParametertypAkzeptiert jede EingabeErgebnistypGeprüft
anyjaanynein: Fehler kompilieren
unknownjaunknownja, aber du musst vor der Verwendung einengen
T (generisch)jaan die Eingabe gebundenja, mit dem genauen Typ

Inferenz von Typargumenten

Meist leitet TypeScript die Typargumente aus den übergebenen Werten ab. Du kannst sie beim Aufruf auch ausdrücklich in spitzen Klammern angeben; das ist nötig, wenn kein Argument T erwähnt oder wenn die Inferenz etwas wählt, das du nicht willst.

Zwei Ergebnisse der Inferenz solltest du kennen. pair(1, "one") ohne ausdrückliches Argument ist ein Fehler: TypeScript leitet T = number aus dem ersten Argument ab und meldet dann Argument of type 'string' is not assignable to parameter of type 'number'. (TS2345). Und emptyList() ganz ohne Argument bekommt T = unknown, also unknown[], was selten gewollt ist.

Mehrere Typparameter

Eine Funktion kann so viele Typparameter deklarieren, wie sie braucht, durch Kommas getrennt. Jeder wird unabhängig abgeleitet.

mapValues leitet drei Dinge auf einmal ab: K ist "tea" | "cake", V ist number, und R ist string aus der Rückgabe des Callbacks. K extends string ist ein Constraint, das auf der nächsten Seite behandelt wird.

Generische Interfaces und Typaliasse

Auch Typen können Parameter nehmen. Du schreibst dann das Argument, wenn du den Typ verwendest: Box<number>, ApiResponse<User>. Die meisten eingebauten Collection-Typen funktionieren so: Array<T>, Map<K, V>, Promise<T>, Record<K, V>.

Result<T, E = string> zeigt auch einen Standard-Typparameter: Result<number> bedeutet Result<number, string>. Wie optionale Funktionsparameter müssen Parameter mit Standardwert nach den Pflichtparametern stehen.

Generische Klassen

Eine Klasse nimmt Typparameter hinter ihrem Namen, und jede Instanz legt sie fest. Felder, Methoden und Konstruktorparameter können sie alle verwenden.

Nimmt der Konstruktor ein T, kannst du das ausdrückliche Argument weglassen: new Box(5) leitet Box<number> ab. Statische Member gehören zur Klasse selbst, nicht zu einer Instanz, daher können sie den Typparameter der Klasse nicht verwenden (Static members cannot reference class type parameters., TS2302).

Generische Arrow Functions und Funktionstypen

Die Liste der Typparameter steht vor der Parameterliste. Dieselbe Syntax beschreibt einen generischen Funktionstyp.

const last = <T>(items: T[]): T | undefined => items[items.length - 1];

type Mapper = <T, R>(items: T[], fn: (item: T) => R) => R[];
const mapAll: Mapper = (items, fn) => items.map(fn);

// In a .tsx file, <T> looks like a JSX tag. Add a trailing comma:
const lastTsx = <T,>(items: T[]) => items[items.length - 1];

Literaltypen mit const-Typparametern ableiten

Standardmäßig leitet ein Generic erweiterte Typen ab: pair("a", "b") ergibt [string, string]. Wenn die genauen Werte zählen (Routennamen, Eventnamen, Spaltenlisten), markiere den Parameter als const (ab TypeScript 5.0), dann wird er so abgeleitet, als wäre das Argument mit as const geschrieben:

Der Aufrufer schreibt ein gewöhnliches Array, und die Funktion behält die Literaltypen.

Wann man Generics nicht verwendet

Ein Typparameter lohnt sich, wenn er zwei Dinge verbindet: einen Parameter und den Rückgabetyp, zwei Parameter oder einen Parameter und einen Callback. Kommt T nur einmal vor, bringt es nichts, und ein einfacher Typ ist klarer.

// Pointless: T is used once, so it is just a longer way to write unknown
function logValue<T>(value: T): void {
  console.log(value);
}

// Clearer
function logValueSimple(value: unknown): void {
  console.log(value);
}

// Also pointless: returns T but nothing connects T to an argument,
// so the caller is really just asserting a type
function parseJson<T>(text: string): T {
  return JSON.parse(text);
}

Der letzte Fall ist häufig und irreführend: parseJson<User>(text) sieht typsicher aus, prüft aber nichts, genau wie JSON.parse(text) as User. Gib stattdessen unknown zurück und validiere. Weitere Zeichen für übertriebenen Einsatz: ein Typparameter, der immer dasselbe Argument bekommt, oder einer, den eine Union aus zwei bekannten Typen ersetzen könnte.

Für Generics, die etwas über T wissen müssen (dass es eine length hat oder dass K ein Schlüssel von T ist), sind Constraints mit extends der nächste Schritt.

Häufig gestellte Fragen

Was sind Generics in TypeScript?

Generics sind Typparameter: Platzhalter wie T, die bei jeder Verwendung einer Funktion, eines Interfaces, eines Typalias oder einer Klasse mit einem echten Typ gefüllt werden. function first<T>(items: T[]): T | undefined funktioniert für jedes Array, und das Ergebnis hat den Elementtyp des übergebenen Arrays, also ist first([1, 2]) vom Typ number | undefined und first(["a"]) vom Typ string | undefined.

Was ist der Unterschied zwischen Generics und any?

any schaltet die Typprüfung ab: Der Wert, der hineingeht, und der, der herauskommt, haben keine Beziehung, und das Ergebnis ist ebenfalls any. Ein Generic behält die Beziehung: identity<T>(x: T): T gibt genau den Typ zurück, den du übergeben hast. Nimm unknown, wenn du alles akzeptierst, den Typ aber nicht zurückgeben musst, und ein Generic, wenn doch.

Was bedeutet <T> in TypeScript?

Es deklariert einen Typparameter namens T. In function wrap<T>(value: T) ist T eine Typvariable, die TypeScript bei jedem Aufruf aus dem Argument füllt oder die du ausdrücklich übergibst, etwa wrap<string>("a"). Der Name T ist nur eine Konvention; längere Namen wie TItem oder Key funktionieren genauso.

Wie schreibe ich in TypeScript eine generische Arrow Function?

Setze die Liste der Typparameter vor die Parameter: const first = <T>(items: T[]): T | undefined => items[0];. In einer .tsx-Datei würde <T> als JSX-Tag gelesen, schreibe dort also <T,> mit einem Komma am Ende oder <T extends unknown>.

Wie lege ich in TypeScript einen Standardtyp für ein Generic fest?

Ergänze = Type hinter dem Parameter: interface ApiResponse<T = unknown> { data: T }. Dann bedeutet ApiResponse ohne Argument ApiResponse<unknown>. Parameter mit Standardwert müssen nach denen ohne stehen, wie bei optionalen Funktionsparametern.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S