Generics TypeScript avec Claude Code : keyof, contraintes et types API
Apprenez les Generics TypeScript avec Claude Code: constraints, keyof, mapped types, types API et vérification tsc.
Pourquoi un prompt vague rend les generics fragiles
Les Generics TypeScript permettent à une fonction, un type ou une classe de fonctionner avec plusieurs formes de données tout en gardant le lien entre l’entrée et la sortie. La difficulté pour les débutants n’est pas la lettre T. Le vrai risque est de demander à Claude Code “rends ça générique” sans préciser ce qui doit rester flexible et ce qui doit être contraint. Le résultat peut sembler réutilisable, mais dépendre de any, de unknown ou d’un Record<string, unknown> trop large.
Dans un vrai produit, les generics sont proches des réponses d’API, des formulaires, des comptes, de la facturation, des événements analytics et des CTA de vente. Si le type est trop large à cet endroit, l’erreur ne se limite pas à une mauvaise autocomplétion : un formulaire de lead, un checkout ou un événement de conversion peut accepter une donnée invalide. La règle de Masa est donc simple : demander à Claude Code un exemple valide et un exemple qui doit échouer à la compilation.
Le flux mental de cet article est le suivant :
valeur d'entrée -> capturée comme T -> clé limitée par keyof T -> forme transformée par mapped types -> contrat vérifié par tsc
La syntaxe utilisée ici a été vérifiée dans la documentation officielle TypeScript : Generics, opérateur keyof, Mapped Types et Conditional Types. Pour compléter le workflow Claude Code, lisez aussi les conseils TypeScript avec Claude Code et les utility types avec Claude Code.
La grille de revue avant le code
Les generics sont des outils de compilation. T n’est pas une variable au runtime, mais un paramètre de type qui aide le compilateur à mémoriser quel type entre et quel type doit sortir. Ici, extends signifie surtout “accepte uniquement les types qui respectent cette forme”. keyof T produit l’ensemble des noms de propriétés de T. Un mapped type parcourt ces noms et construit un nouveau type.
Avant de laisser Claude Code modifier un dépôt, donnez-lui un contrat clair :
| Question | Information à donner à Claude Code | Point de revue |
|---|---|---|
Que représente T ? | Objet métier, DTO ou modèle de formulaire | Le résultat ne perd pas le type original |
| Que faut-il contraindre ? | K extends keyof T, E extends ApiError, T extends object | Les appels invalides échouent à la compilation |
| Comment vérifier ? | @ts-expect-error, Expect, commande stricte tsc | Le mauvais exemple échoue réellement |
Cette grille évite un échec fréquent : Claude Code écrit du code qui compile seulement parce qu’il ajoute un cast final. Un cast peut être acceptable lorsqu’il documente une transformation que TypeScript n’infère pas, mais il ne doit pas cacher un design trop large.
Cas 1 : dédupliquer avec une clé sûre
Le premier cas pratique est uniqueBy, utile pour des lignes d’API, des imports CSV, des tableaux d’administration et des listes UI. Si key est typé comme string, l’appelant peut passer une propriété inexistante. Avec K extends keyof T, la clé doit être une vraie propriété du type des éléments.
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));
Dans le prompt, demandez explicitement : “utilise des overloads, limite key à keyof T, et ajoute un appel @ts-expect-error pour une clé inexistante”. Sans cela, Claude Code peut proposer key: string puis item[key as keyof T], ce qui repousse le problème au runtime.
Cas 2 : typer les réponses API sans optional flou
Le deuxième cas est un type de réponse API. Beaucoup de projets écrivent data?: T et error?: ApiError dans la même interface. C’est rapide, mais l’appelant doit vérifier chaque fois si data existe, si error existe ou si l’état est incohérent. Une union discriminée rend l’état explicite : succès avec data, échec avec error, et le champ ok réduit le type.
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());
Ce modèle convient bien à Claude Code, car le prompt peut décrire à la fois la validation runtime et le contrat de type. Pour aller plus loin côté backend, consultez le développement API avec Claude Code et les tests API avec Claude Code.
Cas 3 : état de formulaire avec mapped types
Le troisième cas est l’état d’un formulaire. On part d’un modèle métier et l’on dérive, pour chaque champ, un état contenant value, dirty et errors. Les mapped types évitent de répéter les noms de champs et gardent le type de chaque valeur : email reste string, seats reste number, newsletter reste 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);
Le point à surveiller est le cast après Object.fromEntries. Il ne signifie pas que l’on fait confiance à une donnée externe ; il explique que la transformation conserve les mêmes clés, même si TypeScript n’infère pas exactement le mapped type. Demandez à Claude Code d’expliquer chaque cast.
Vérifier avec tsc et des tests de type
Un article sur les generics doit être vérifié. Placez les exemples dans examples/generics.ts, puis lancez une compilation stricte.
{
"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
Pour tester seulement les types, ajoutez de petites assertions de compilation. Elles ne font rien en JavaScript, mais échouent si le type attendu change.
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.");
Modèles de prompts pour la revue Claude Code
Après la génération, utilisez Claude Code comme reviewer de types.
Modèle 1 : revue d'un helper générique
Relis cette fonction TypeScript.
Objectif : dédupliquer un tableau selon une clé choisie.
Contraintes : key doit être K extends keyof T. Interdire any. Ajouter @ts-expect-error pour une clé inexistante.
Sortie : problèmes, version corrigée, commande tsc de vérification.
Modèle 2 : revue d'un type de réponse API
Relis ce type de réponse API.
Objectif : succès avec data, échec avec error.
Contraintes : éviter les champs optionnels flous comme data?: T. Confirmer que ok réduit le type.
Sortie : exemple d'appel sûr, exemple d'échec et tests de type à ajouter.
Modèle 3 : revue de mapped types
Relis ce mapped type.
Objectif : dériver un état de champs depuis un modèle de formulaire.
Contraintes : expliquer keyof, T[K], readonly, propriétés optionnelles et tout cast nécessaire.
Sortie : flux de type, cas fragiles, correction minimale.
Modèle 4 : audit de type avant PR
Audite les generics, conditional types et mapped types de ce diff.
Vérifier : any, Record trop large, paramètres de type inutiles, manque de @ts-expect-error, validation runtime manquante.
Sortie : bloqueurs, améliorations mineures, tests supplémentaires par priorité.
Pièges à détecter tôt
| Piège | Ce qui casse | Habitude plus sûre |
|---|---|---|
Utiliser any pour paraître générique | Le retour perd son information | Capturer la relation avec T |
Taper la clé comme string | Des propriétés absentes compilent | Utiliser K extends keyof T |
Abuser de Record<string, unknown> | Les propriétés concrètes disparaissent | Préférer object hors dictionnaire |
| Tout rendre optionnel dans l’API | L’appelant ne peut pas faire confiance à data ou error | Utiliser une union discriminée |
| Ne pas expliquer les casts | La revue ne peut pas juger la sécurité | Documenter l’invariant avant le cast |
La différence entre T extends object et T extends Record<string, unknown> compte. Un formulaire a souvent seulement besoin d’être un objet. Un helper de dictionnaire avec clés arbitraires peut justifier Record.
CTA : relier les types aux chemins de revenu
Les generics ne sont pas un exercice esthétique. Si les types sont faibles dans les formulaires, le checkout, les payloads API, les templates de produit ou les événements analytics, le chemin lecteur-client peut casser. Commencez avec la fiche gratuite Claude Code, utilisez les produits et templates pour les prompts réutilisables, puis passez à la formation ou consultation Claude Code si l’équipe doit standardiser CLAUDE.md, les règles de revue, la CI et le rollout.
Dans votre dépôt, ciblez d’abord les types proches du business : compte, facturation, formulaire, réponse API, tracking. Demandez à Claude Code non seulement “est-ce que ça compile ?”, mais aussi “cette erreur de type peut-elle casser une conversion ?”.
Résultat vérifié
Après essai, Masa a constaté que séparer le prompt d’implémentation du prompt de revue de types rend le résultat plus stable. D’abord, on génère le helper ; ensuite, on audite any, les oublis de keyof, les réponses API trop optionnelles et l’absence de @ts-expect-error. Les exemples uniqueBy et état de formulaire sont utiles, car tsc --noEmit --strict prouve les deux côtés du contrat : les appels valides compilent et les appels volontairement invalides sont rejetés.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Échelle de sécurité des permissions Claude Code
Passer du read-only aux éditions limitées, preuves et checks de déploiement sans perdre le contrôle.
Claude Code Small PR Proof Pack : rendre les petits changements reviewables
Un pack de preuve pour PR Claude Code : diff, vérifications, URL publique, CTA et rollback.
Gate de review avant commit avec Claude Code
Review avant commit avec Claude Code : diff, build, URL publique, liens Gumroad, CTA consultation, tests manquants et fichiers hors scope.