Generics in TypeScript: Wiederverwendbarer und typsicherer Code

Generics erlauben dir, Funktionen und Klassen zu schreiben, die mit beliebigen Typen arbeiten – ohne die Typsicherheit zu verlieren. So nutzt du sie richtig.

Teilen

Stell dir vor, du schreibst eine Funktion, die das erste Element eines Arrays zurückgibt. Sie soll für Zahlen genauso funktionieren wie für Strings oder Objekte. Du könntest überall any verwenden – aber damit wirfst du genau das weg, wofür du TypeScript nutzt: die Typsicherheit. Generics lösen dieses Problem elegant. Sie sind Platzhalter für Typen, die erst beim Aufruf konkret werden. In diesem Beitrag lernst du, wie du Generics verstehst und im Alltag sinnvoll einsetzt.

Das Problem: Flexibilität ohne Typverlust

Schauen wir uns zuerst an, warum any keine gute Lösung ist. Sobald du any verwendest, verlierst du jede Information über den Typ – TypeScript kann dich nicht mehr warnen.

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

const zahl = erstes([1, 2, 3]);
// zahl ist vom Typ "any" – TypeScript weiß nichts mehr
zahl.toUpperCase(); // Kein Fehler beim Kompilieren, aber Absturz zur Laufzeit!

Der Aufruf von toUpperCase() auf einer Zahl geht zur Laufzeit schief, aber TypeScript merkt nichts. Genau hier kommen Generics ins Spiel.

Deine erste generische Funktion

Ein Generic wird in spitzen Klammern deklariert, üblicherweise mit dem Buchstaben T (für "Type"). Dieser Platzhalter steht für den Typ, der beim Aufruf eingesetzt wird.

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

const zahl = erstes([1, 2, 3]);   // T wird zu number
const wort = erstes(["a", "b"]);  // T wird zu string

zahl.toFixed(2);      // OK, TypeScript weiß: zahl ist number
wort.toUpperCase();   // OK, TypeScript weiß: wort ist string

Das Schöne daran: TypeScript leitet den Typ automatisch ab (Type Inference). Du musst erstes<number>([1, 2, 3]) nicht explizit schreiben – der Compiler erkennt aus dem Argument, dass T gleich number ist. Der zurückgegebene Wert trägt denselben Typ, und du behältst volle Autovervollständigung.

Generics mit mehreren Typparametern

Du bist nicht auf einen einzigen Platzhalter beschränkt. Eine Funktion, die zwei Werte zu einem Paar kombiniert, braucht zum Beispiel zwei unabhängige Typen.

function paar<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const p = paar("Alter", 30);
// p hat den Typ [string, number]

const [label, wert] = p;
// label: string, wert: number

Die Namen A und B sind frei wählbar – aussagekräftige Namen wie Key und Value erhöhen oft die Lesbarkeit. Wichtig ist nur, dass jeder Platzhalter für einen eigenen Typ steht.

Typen einschränken mit extends

Manchmal soll ein Generic nicht wirklich jeden Typ annehmen, sondern nur solche mit bestimmten Eigenschaften. Mit extends legst du eine Bedingung fest. Das folgende Beispiel akzeptiert nur Objekte, die eine length-Eigenschaft besitzen.

interface HatLaenge {
  length: number;
}

function logLaenge<T extends HatLaenge>(wert: T): T {
  console.log(`Länge: ${wert.length}`);
  return wert;
}

logLaenge("Hallo");        // OK, Strings haben length
logLaenge([1, 2, 3]);      // OK, Arrays haben length
logLaenge({ length: 10 }); // OK
logLaenge(42);             // Fehler: number hat keine length

Der Vorteil: Innerhalb der Funktion darfst du auf wert.length zugreifen, weil TypeScript garantiert, dass diese Eigenschaft existiert. Ohne die Einschränkung wäre der Zugriff ein Fehler.

Ein praktisches Beispiel: eine typsichere API-Antwort

Generics glänzen besonders bei wiederverwendbaren Strukturen. Angenommen, deine API verpackt jede Antwort in ein gemeinsames Format mit data und status. Mit einem generischen Interface bleibt der eigentliche Datentyp flexibel.

interface ApiAntwort<T> {
  data: T;
  status: number;
  ok: boolean;
}

interface Benutzer {
  id: number;
  name: string;
}

async function ladeBenutzer(id: number): Promise<ApiAntwort<Benutzer>> {
  const res = await fetch(`/api/users/${id}`);
  const data = await res.json();
  return { data, status: res.status, ok: res.ok };
}

const antwort = await ladeBenutzer(1);
console.log(antwort.data.name); // TypeScript kennt name als string

Dasselbe Interface funktioniert für jeden Datentyp: ApiAntwort<Benutzer>, ApiAntwort<Produkt[]> oder ApiAntwort<string>. Du schreibst die Struktur einmal und nutzt sie überall typsicher.

Standardwerte für Typparameter

Wie bei Funktionsargumenten kannst du auch Typparametern einen Standardwert geben. Das ist praktisch, wenn ein Typ in den meisten Fällen gleich ist, aber gelegentlich überschrieben werden soll.

interface Container<T = string> {
  inhalt: T;
}

const a: Container = { inhalt: "Text" };        // T ist string (Standard)
const b: Container<number> = { inhalt: 42 };    // T explizit number

So bleibt die häufige Nutzung kurz, während die seltenere Variante weiterhin möglich ist.

Fazit

Generics sind das Werkzeug, mit dem du in TypeScript Wiederverwendbarkeit und Typsicherheit gleichzeitig erreichst. Statt Code für jeden Typ zu duplizieren oder mit any die Kontrolle abzugeben, schreibst du eine Lösung, die sich an den jeweiligen Typ anpasst. Fang klein an: Ersetze ein any in einer Hilfsfunktion durch ein <T> und beobachte, wie die Autovervollständigung zurückkehrt. Mit extends für Einschränkungen und generischen Interfaces für Datenstrukturen hast du schon die wichtigsten Bausteine in der Hand. Wenn du Generics einmal verinnerlicht hast, willst du sie nicht mehr missen.