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.
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:
| Frage | Information für Claude Code | Prüfung |
|---|---|---|
Wofür steht T? | Domain-Objekt, DTO oder Formularmodell | Das Ergebnis verliert den Ursprungstyp nicht |
| Was wird begrenzt? | K extends keyof T, E extends ApiError, T extends object | Ungültige Aufrufe scheitern beim Kompilieren |
| Wie wird verifiziert? | @ts-expect-error, Expect, strenger tsc-Befehl | Das 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
| Falle | Was bricht | Sicherere Gewohnheit |
|---|---|---|
any als scheinbare Generik | Rückgabetyp verliert Information | Beziehung mit T erfassen |
Key als string | Fehlende Properties kompilieren | K extends keyof T nutzen |
Zu viel Record<string, unknown> | Konkrete Properties verschwinden | Ohne Dictionary lieber object prüfen |
| API-Felder alle optional | Caller vertrauen data oder error nicht | Discriminated union nutzen |
| Unerklärte Casts | Review kann Sicherheit nicht bewerten | Invariant 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.
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.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Permission Safety Ladder: Zugriff kontrolliert erweitern
Von read-only zu begrenzten Änderungen, Prüfbefehlen und Deploy-Checks mit klarer Kontrolle.
Claude Code Small PR Proof Pack: kleine Änderungen reviewbar machen
Ein Proof Pack für Claude-Code-PRs: Diff, Checks, öffentliche URL, CTA-Pfad und Rollback.
Claude-Code-Review-Gate vor dem Commit
Vor dem Commit mit Claude Code prüfen: Diff, Build, öffentliche URL, Gumroad-Links, Beratung-CTA, fehlende Tests und fremde Dateien.