Tips & Tricks (Aktualisiert: 2.6.2026)

TypeScript Generics mit Claude Code: keyof, Constraints und API-Typen

Lerne TypeScript Generics mit Claude Code: Constraints, keyof, mapped types, API-Ergebnisse und tsc-Prüfung.

TypeScript Generics mit Claude Code: keyof, Constraints und API-Typen

Warum ein vager Prompt schwache Generics erzeugt

TypeScript Generics machen Funktionen, Typen und Klassen wiederverwendbar, ohne die Beziehung zwischen Eingabe und Ausgabe zu verlieren. Das Problem für Einsteiger ist nicht der Buchstabe T. Das Problem ist ein Prompt wie “mach das generisch”, ohne zu sagen, welche Teile flexibel bleiben dürfen und welche Teile eingeschränkt werden müssen. Claude Code kann dann Code liefern, der wiederverwendbar aussieht, aber in Wahrheit auf any, unknown oder ein zu breites Record<string, unknown> ausweicht.

In einem echten Produkt liegen Generics oft nah an API-Daten, Formularen, Accounts, Billing, Analytics-Events und Produkt-CTAs. Wenn der Typ dort zu weit ist, geht es nicht nur um schlechtere Autovervollständigung. Ein Lead-Formular, Checkout oder Tracking-Event kann falsche Daten akzeptieren. Masa arbeitet deshalb mit einer einfachen Regel: Claude Code soll immer ein gültiges Beispiel und ein absichtlich falsches Beispiel liefern, das bei tsc scheitern muss.

Das Denkmodell für diesen Artikel:

Eingabewert -> als T erfasst -> Schlüssel durch keyof T begrenzt -> Struktur mit mapped types verändert -> Vertrag mit tsc geprüft

Die Syntax in diesem Artikel wurde mit der offiziellen TypeScript-Dokumentation abgeglichen: Generics, keyof Type Operator, Mapped Types und Conditional Types. Für den größeren Claude-Code-Workflow passen auch TypeScript-Tipps mit Claude Code und Utility Types mit Claude Code.

Die Prüftabelle vor dem Code

Generics sind Werkzeuge der Kompilierzeit. T ist keine Runtime-Variable, sondern ein Typparameter, mit dem der Compiler speichert, welcher Typ hineingeht und welcher Typ herauskommen soll. extends bedeutet in diesem Kontext: “akzeptiere nur Typen, die diese Form erfüllen”. keyof T erzeugt die Menge der Eigenschaftsnamen von T. Ein mapped type läuft über diese Namen und baut daraus einen neuen Typ.

Bevor Claude Code ein Repository ändert, hilft ein klarer Vertrag:

FrageInformation für Claude CodePrüfung
Wofür steht T?Domain-Objekt, DTO oder FormularmodellDas Ergebnis verliert den Ursprungstyp nicht
Was wird begrenzt?K extends keyof T, E extends ApiError, T extends objectUngültige Aufrufe scheitern beim Kompilieren
Wie wird verifiziert?@ts-expect-error, Expect, strenger tsc-BefehlDas schlechte Beispiel scheitert wirklich

Diese Tabelle verhindert einen typischen Fehler: Claude Code schreibt Code, der nur deshalb kompiliert, weil am Ende ein Cast steht. Ein Cast kann sinnvoll sein, wenn er eine Transformation dokumentiert, die TypeScript nicht genau ableitet. Er darf aber kein zu breites Design verstecken.

Fall 1: Deduplizieren mit sicherem Schlüssel

Der erste praktische Fall ist uniqueBy, nützlich für API-Zeilen, CSV-Importe, Admin-Tabellen und UI-Listen. Wenn key nur string ist, kann der Caller eine Eigenschaft übergeben, die nicht existiert. Mit K extends keyof T muss der Schlüssel eine echte Eigenschaft des Elementtyps sein.

type User = {
  id: string;
  email: string;
  role: "admin" | "editor";
  score: number;
};

function uniqueBy<T>(items: readonly T[]): T[];
function uniqueBy<T, K extends keyof T>(items: readonly T[], key: K): T[];
function uniqueBy<T, K extends keyof T>(items: readonly T[], key?: K): T[] {
  const seen = new Set<unknown>();
  const output: T[] = [];

  for (const item of items) {
    const value = key === undefined ? item : item[key];
    if (seen.has(value)) continue;
    seen.add(value);
    output.push(item);
  }

  return output;
}

const users: User[] = [
  { id: "u_1", email: "masa@example.com", role: "admin", score: 92 },
  { id: "u_2", email: "editor@example.com", role: "editor", score: 88 },
  { id: "u_1", email: "masa+copy@example.com", role: "admin", score: 70 },
];

const byId = uniqueBy(users, "id");
const byRole = uniqueBy(users, "role");

// @ts-expect-error "missing" is not a key of User.
uniqueBy(users, "missing");

console.log(byId.map((user) => user.id));
console.log(byRole.map((user) => user.role));

Der Prompt sollte präzise sein: “Verwende Overloads, begrenze key auf keyof T und füge einen @ts-expect-error-Aufruf für einen fehlenden Schlüssel hinzu.” Ohne diese Vorgabe kann Claude Code key: string mit item[key as keyof T] erzeugen. Das verschiebt das Risiko zur Laufzeit.

Fall 2: API-Antworten ohne optionales Durcheinander

Der zweite Fall ist ein API-Ergebnistyp. Viele Codebasen nutzen data?: T und error?: ApiError in einem Interface. Das wirkt bequem, zwingt aber jeden Caller zu prüfen, ob data, error, beides oder nichts vorhanden ist. Eine discriminated union macht den Zustand explizit: Erfolg hat data, Fehler hat error, und ok verengt den Typ.

type ApiError = {
  code: string;
  message: string;
  retryable: boolean;
};

type ApiResult<T, E extends ApiError = ApiError> =
  | { ok: true; status: number; data: T; error?: never }
  | { ok: false; status: number; error: E; data?: never };

type UserDto = {
  id: string;
  name: string;
  plan: "free" | "pro";
};

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function parseUserResponse(json: unknown): ApiResult<UserDto> {
  if (
    isRecord(json) &&
    typeof json.id === "string" &&
    typeof json.name === "string" &&
    (json.plan === "free" || json.plan === "pro")
  ) {
    return {
      ok: true,
      status: 200,
      data: { id: json.id, name: json.name, plan: json.plan },
    };
  }

  return {
    ok: false,
    status: 422,
    error: {
      code: "INVALID_USER_RESPONSE",
      message: "User response does not match the expected shape.",
      retryable: false,
    },
  };
}

function unwrap<T, E extends ApiError>(result: ApiResult<T, E>): T {
  if (result.ok) {
    return result.data;
  }

  throw new Error(`${result.error.code}: ${result.error.message}`);
}

const parsed = parseUserResponse({ id: "u_1", name: "Masa", plan: "pro" });
const user = unwrap(parsed);
console.log(user.name.toUpperCase());

Dieses Muster passt gut zu Claude Code, weil der Prompt Runtime-Validierung und Typvertrag gleichzeitig beschreiben kann. Für den größeren Backend-Kontext siehe API-Entwicklung mit Claude Code und API-Tests mit Claude Code.

Fall 3: Formularzustand mit mapped types

Der dritte Fall ist ein Formularzustand. Aus einem fachlichen Modell wird für jedes Feld ein Zustand mit value, dirty und errors abgeleitet. Mapped types verhindern doppelte Feldnamen und behalten die Werttypen: email bleibt string, seats bleibt number, newsletter bleibt boolean.

type FieldState<T> = {
  value: T;
  dirty: boolean;
  errors: string[];
};

type FormState<T extends object> = {
  [K in keyof T]: FieldState<T[K]>;
};

function createFormState<T extends object>(initial: T): FormState<T> {
  const entries = Object.entries(initial).map(([key, value]) => [
    key,
    { value, dirty: false, errors: [] },
  ]);

  return Object.fromEntries(entries) as FormState<T>;
}

function setField<T extends object, K extends keyof T>(
  state: FormState<T>,
  key: K,
  value: T[K],
): FormState<T> {
  return {
    ...state,
    [key]: { value, dirty: true, errors: [] },
  } as FormState<T>;
}

type SignupForm = {
  email: string;
  seats: number;
  newsletter: boolean;
};

const form = createFormState<SignupForm>({
  email: "team@example.com",
  seats: 2,
  newsletter: true,
});

const updated = setField(form, "seats", 3);

// @ts-expect-error seats must be a number.
setField(form, "seats", "three");

console.log(updated.seats.value);

Der Cast nach Object.fromEntries ist der Review-Punkt. Er vertraut nicht blind externen Daten, sondern dokumentiert, dass die Transformation dieselben Schlüssel bewahrt, obwohl TypeScript den exakten mapped type nicht erkennt. Claude Code sollte jeden Cast begründen.

Mit tsc und Typ-Tests prüfen

Ein Generics-Artikel sollte kompilieren. Lege die Beispiele in examples/generics.ts und führe eine strenge Prüfung aus.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noEmit": true,
    "lib": ["ES2022", "DOM"]
  },
  "include": ["examples/**/*.ts"]
}
npm install --save-dev typescript
npx tsc --noEmit --strict --lib ES2022,DOM examples/generics.ts

Für reine Typprüfungen helfen kleine Compile-Time-Assertions. Sie laufen nicht in JavaScript, schlagen aber fehl, wenn der erwartete Typ nicht stimmt.

type Equal<A, B> =
  (<T>() => T extends A ? 1 : 2) extends
  (<T>() => T extends B ? 1 : 2)
    ? true
    : false;

type Expect<T extends true> = T;

type PickReadonly<T, K extends keyof T> = {
  readonly [P in K]: T[P];
};

type Account = {
  id: string;
  email: string;
  seats: number;
};

type PublicAccount = PickReadonly<Account, "id" | "email">;

type PublicAccountCheck = Expect<
  Equal<PublicAccount, { readonly id: string; readonly email: string }>
>;

const leaked: PublicAccount = {
  id: "a_1",
  email: "team@example.com",
  // @ts-expect-error seats is intentionally not part of PublicAccount.
  seats: 10,
};

console.log("Type checks are compile-time only.");

Claude-Code-Templates für Typreviews

Nach der Generierung sollte Claude Code eine gezielte Typprüfung übernehmen.

Template 1: Review eines generischen Helpers
Prüfe diese TypeScript-Funktion.
Ziel: ein Array nach einem gewählten Key deduplizieren.
Constraints: key muss K extends keyof T sein. any ist verboten. Füge @ts-expect-error für einen fehlenden Key hinzu.
Ausgabe: Probleme, korrigierter Code, tsc-Befehl zur Verifikation.
Template 2: Review eines API-Ergebnistyps
Prüfe diesen API-Response-Typ.
Ziel: Erfolg hat data, Fehler hat error.
Constraints: keine vagen optionalen Felder wie data?: T. Bestätige, dass ok den Typ verengt.
Ausgabe: sicheres Caller-Beispiel, Fehlerbeispiel, zusätzliche Typ-Tests.
Template 3: Review von mapped types
Prüfe diesen mapped type.
Ziel: Feldzustand aus einem Formularmodell ableiten.
Constraints: erkläre keyof, T[K], readonly, optionale Properties und nötige Casts.
Ausgabe: Typfluss, fragile Fälle, kleinste sichere Korrektur.
Template 4: Typ-Audit vor dem PR
Auditiere Generics, conditional types und mapped types in diesem Diff.
Prüfen: any, zu breites Record, unnötige Typparameter, fehlendes @ts-expect-error, fehlende Runtime-Validierung.
Ausgabe: Blocker, kleine Verbesserungen, zusätzliche Tests nach Priorität.

Häufige Fallen

FalleWas brichtSicherere Gewohnheit
any als scheinbare GenerikRückgabetyp verliert InformationBeziehung mit T erfassen
Key als stringFehlende Properties kompilierenK extends keyof T nutzen
Zu viel Record<string, unknown>Konkrete Properties verschwindenOhne Dictionary lieber object prüfen
API-Felder alle optionalCaller vertrauen data oder error nichtDiscriminated union nutzen
Unerklärte CastsReview kann Sicherheit nicht bewertenInvariant vor dem Cast dokumentieren

Der Unterschied zwischen T extends object und T extends Record<string, unknown> ist wichtig. Ein Formularmodell muss meist nur ein Objekt sein. Ein Dictionary-Helper mit beliebigen String-Schlüsseln kann dagegen Record brauchen.

CTA: Typensicherheit mit Umsatzpfaden verbinden

Generics sind kein reines Sprachspiel. Wenn Typen in Formularen, Checkout, API-Payloads, Produkt-Templates oder Analytics-Events schwach sind, kann der Weg vom Leser zum Kunden brechen. Starte mit dem kostenlosen Claude Code Cheatsheet, nutze Produkte und Templates für wiederverwendbare Prompts und gehe zu Claude Code Training und Beratung, wenn ein Team CLAUDE.md, Review-Regeln, CI und Rollout standardisieren will.

Im eigenen Repository solltest du zuerst die Typen nahe am Geschäft prüfen: Account, Billing, Formular, API-Response und Tracking. Frage Claude Code nicht nur “kompiliert das?”, sondern auch “kann dieser Typfehler eine Conversion zerstören?”.

Geprüftes Ergebnis

Beim Ausprobieren zeigte sich für Masa: Implementierungs-Prompt und Typreview-Prompt getrennt zu halten, liefert stabilere Ergebnisse. Erst wird der Helper generiert, danach prüft Claude Code gezielt any, fehlendes keyof, zu optionale API-Ergebnisse und fehlende @ts-expect-error-Fälle. Die Beispiele uniqueBy und Formularzustand sind nützlich, weil tsc --noEmit --strict beide Seiten des Vertrags beweist: gültige Aufrufe kompilieren, absichtlich falsche Aufrufe werden abgelehnt.

#Claude Code #TypeScript #generics #type safety #design patterns
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.