Zurück zum Blog
Jan. 2026·3 Min. LesezeitKI-generiert
Sprache:DeutschEnglishFrançaisÖsterreichischKlingonisch Originalfassung: English

TypeScript-Generics verstehen

#typescript#javascript

Generics sind das Feature, über das man direkt stolpert, nachdem man sich mit den grundlegenden TypeScript-Typen angefreundet hat – und das, sobald es klickt, zu einem der am häufigsten genutzten Werkzeuge im Kasten wird. Die Kernidee ist klein: Statt eine Funktion oder einen Typ für einen bestimmten Typ zu schreiben, schreibt man sie für „was auch immer übergeben wird", und TypeScript verfolgt diesen Typ für dich durch den Rest der Signatur.

Das Problem, das Generics lösen

Angenommen, du willst eine Funktion, die das erste Element eines Arrays zurückgibt.

function firstOfNumbers(arr: number[]): number {
  return arr[0];
}

function firstOfStrings(arr: string[]): string {
  return arr[0];
}

Zwei Funktionen, identische Logik, unterschiedliche Typen. Man könnte sie mit any typisieren:

function first(arr: any[]): any {
  return arr[0];
}

Das funktioniert, aber man hat alle Information weggeworfen. first([1, 2, 3]) hat nun den Typ any statt number, sodass nichts weiter unten einen Fehler wie first([1, 2, 3]).toUpperCase() abfängt.

Generics bewahren die Verbindung zwischen Eingabe- und Ausgabetyp:

function first<T>(arr: T[]): T {
  return arr[0];
}

const n = first([1, 2, 3]);       // T inferred as number, n: number
const s = first(['a', 'b', 'c']); // T inferred as string, s: string

T ist ein Typparameter – ein Platzhalter, den TypeScript anhand dessen füllt, was man tatsächlich übergibt, genauso wie ein Funktionsparameter mit einem Wert gefüllt wird. Man musste nicht first<number>([1, 2, 3]) schreiben; TypeScript leitet T aus dem Argument ab.

Einschränken, was T sein darf

Ein uneingeschränktes T kann alles sein, was bedeutet, dass man nicht annehmen kann, es habe irgendeine bestimmte Form. Das kompiliert nicht:

function getLength<T>(item: T): number {
  return item.length; // Error: Property 'length' does not exist on type 'T'
}

TypeScript beschwert sich zu Recht – T könnte number sein, das kein .length hat. Schränke es mit extends ein:

function getLength<T extends { length: number }>(item: T): number {
  return item.length; // OK — T is guaranteed to have a `length` property
}

getLength('hello');      // OK, strings have .length
getLength([1, 2, 3]);    // OK, arrays have .length
getLength(42);           // Error: number doesn't satisfy the constraint

extends bedeutet hier keine Klassenvererbung – es heißt „T muss dieser Form zuweisbar sein". Jeder Typ mit einer numerischen Eigenschaft .length erfüllt das.

Generische Interfaces und Klassen

Generics sind nicht nur für Funktionen – sie parametrisieren Typen selbst. Eine Box<T>, die einen beliebigen einzelnen Wert halten kann:

interface Box<T> {
  value: T;
}

const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: 'hello' };

Ein realistischeres Beispiel – ein typisierter API-Antwort-Wrapper, der in einer ganzen App verwendet wird:

interface ApiResponse<T> {
  data: T;
  status: number;
  error: string | null;
}

async function fetchUser(id: string): Promise<ApiResponse<User>> {
  const res = await fetch(`/api/users/${id}`);
  const data = await res.json();
  return { data, status: res.status, error: null };
}

Jeder Aufrufer von fetchUser bekommt response.data als User typisiert, nicht als any – das Generic trägt den konkreten Nutzlasttyp durch die gesamte Antwortform, ohne dass man für jede Entität ein eigenes UserApiResponse, PostApiResponse und so weiter schreiben muss.

Mehrere Typparameter

Generics können mehr als einen Parameter haben, was bei Funktionen üblich ist, die zwei unabhängige Dinge kombinieren – eine Map-Funktion ist das klassische Beispiel:

function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}

const lengths = mapArray(['a', 'bb', 'ccc'], (s) => s.length);
// T inferred as string, U inferred as number
// lengths: number[]

T ist der Typ der Eingabeelemente, U ist, was auch immer der Callback zurückgibt – sie müssen nicht übereinstimmen, und TypeScript leitet beide unabhängig voneinander aus dem Aufruf ab.

Standard-Typparameter

Wie Standardwerte bei Funktionsparametern können Generics Defaults haben, nützlich, wenn ein Typparameter meist dasselbe ist, aber überschreibbar bleiben soll:

interface PaginatedResponse<T = unknown> {
  items: T[];
  page: number;
  totalPages: number;
}

function emptyPage(): PaginatedResponse {
  return { items: [], page: 1, totalPages: 1 }; // T defaults to unknown
}

function usersPage(): PaginatedResponse<User> {
  return { items: [], page: 1, totalPages: 1 }; // T explicitly User
}

Ein häufiger Fehler: zu früh zu Generics greifen

Nicht jede Funktion mit einem Parameter muss generisch sein. Wenn eine Funktion nur jemals auf string arbeitet, fügt function process<T extends string>(input: T) einen Typparameter hinzu, der nie variiert und seine Komplexität nie verdient. Generics zahlen sich aus, wenn die Beziehung zwischen Eingabe- und Ausgabetyp über einen Aufruf hinweg tatsächlich erhalten bleiben muss – ein first<T>, das denselben Elementtyp zurückgibt, den es erhielt, eine Box<T>, die beliebige Werte umhüllt, ein mapArray<T, U>, das einen Typ in einen anderen umwandelt. Ändert sich der Typ über alle Verwendungen nie, ist ein konkreter Typ klarer als ein Generic, das niemand unterschiedlich instanziiert.

Fazit

Generics existieren, um eine Beziehung zwischen Typen zu bewahren, die any auslöschen würde und die das Schreiben von N fast identischen Überladungen duplizieren würde. Das mentale Modell, mit dem es klickt: Ein Typparameter ist ein Parameter, genau wie ein Funktionsargument, nur dass man einen Typ übergibt, und TypeScript verfolgt ihn durch den Rest der Signatur genauso, wie es einen Wert verfolgt.

NeuerFeature-Flags: Ein praktischer LeitfadenÄlterEinen LRU-Cache von Grund auf bauen