Kodokon kodokon.com

Fortgeschrittene Generics: Constraints und Standardwerte

Du schränkst deine Typparameter mit extends ein und gibst Standardwerte vor, um ergonomische generische APIs zu bauen.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Ein Generic ohne Constraint ist undurchsichtig: Im Rumpf der Funktion erlaubt dir T gar nichts. Die extends-Klausel legt einen Mindestvertrag für den Typparameter fest, was den Zugriff auf die garantierten Eigenschaften freischaltet und zugleich den präzisen Typ des Aufrufers bewahrt. Genau darin liegt der grundlegende Unterschied zu einem Parameter, der mit einem Interface typisiert ist: Das Constraint bewahrt die Information, das Interface löscht sie.

TYPESCRIPT
function longest<T extends { length: number }>(
  a: T,
  b: T
): T {
  return a.length >= b.length ? a : b;
}

const word = longest("hello", "worldwide");
const list = longest([1, 2], [3, 4, 5]);
word bleibt als string typisiert, list als number[].

Das Duo K extends keyof T ist das im Alltag lohnendste Muster: Es verknüpft zwei Typparameter miteinander und garantiert statisch, dass ein Schlüssel wirklich zum Objekt gehört. Der Rückgabetyp T[K] (ein indizierter Zugriffstyp) folgt der angefragten Eigenschaft ganz genau. Exakt so werden Funktionen wie pick oder pluck aus Utility-Bibliotheken typisiert.

TYPESCRIPT
function getProp<T extends object, K extends keyof T>(
  obj: T,
  key: K
): T[K] {
  return obj[key];
}

const config = { port: 8080, host: "localhost" };
const port = getProp(config, "port");
const host = getProp(config, "host");
port wird als number abgeleitet, host als string; jeder andere Schlüssel wird abgelehnt.

Standardwerte (T = string) machen ein Generic an der Aufrufstelle optional: Nutzer geben das Typargument nur an, wenn sie vom Normalfall abweichen wollen. Kombiniert mit einem Constraint (T extends X = Y) legen sie sowohl die Schranke als auch das implizite Verhalten fest. Der Standardwert muss das Constraint erfüllen, sonst kompiliert schon die Deklaration selbst nicht. Das ist das Standardmuster für HTTP-Clients, Stores und typisierte Event-Emitter.

TYPESCRIPT
interface ApiResponse<T = unknown> {
  status: number;
  data: T;
}

async function fetchJson<T = unknown>(
  url: string
): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  return { status: res.status, data: await res.json() };
}

interface Post { id: number; title: string }
const typed = await fetchJson<Post>("/api/posts/1");
const loose = await fetchJson("/api/health");
Ohne Typargument fällt data auf unknown zurück, nicht auf any.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Was garantiert das Constraint K extends keyof T in einer generischen Signatur?
    • Dass K irgendein beliebiger String ist
    • Dass K ein existierender Schlüssel von T ist, geprüft zur Kompilierzeit
    • Dass T mindestens einen optionalen Schlüssel enthält
    • Dass K und T derselbe Typ sind
  2. Welchen Vorteil hat longest<T extends { length: number }> gegenüber longest(a: { length: number }, b: ...) ohne Generics?
    • Es läuft schneller
    • Es bewahrt den präzisen Typ des Aufrufers im Rückgabewert
    • Es akzeptiert mehr Eingabetypen
  3. Warum ist <T = unknown> als Standardwert besser als <T = any>?
    • unknown kompiliert schneller als any
    • any ist im Strict-Modus verboten
    • unknown zwingt den Aufrufer, den Typ vor der Verwendung zu verengen, any schaltet jede Prüfung ab
    • unknown akzeptiert weniger Werte als any