Lesson 6/10 12 min

Generics

The cake mold: reusable code that keeps the type contract. The course's pivot lesson, taken gently.

Le dilemme de la duplication

Tu viens d'écrire une fonction parfaitement typée pour récupérer le premier élément d'un tableau de nombres :

function premierNombre(liste: number[]): number {
  return liste[0];
}

Puis le besoin arrive : la même chose pour des chaînes. Et là, trois options s'offrent à toi, toutes moches à leur façon.

Option 1 : dupliquer la fonction. On copie-colle en changeant le type. Deux fonctions pour une même idée, et quand la logique change il faut penser à modifier les deux. Mauvais signe.

function premierNombre(liste: number[]): number  { return liste[0]; }
function premierString(liste: string[]): string   { return liste[0]; }
// …et si on a besoin de boolean ? On en fait une troisième ?

Option 2 : passer à any[]. Une seule fonction, certes, mais souviens-toi de la leçon 1 : any efface le contrat. Le retour est any, TypeScript ne sait plus rien de ce qui sort, et tu reperds toute la vérification en aval. C'est comme avoir une caisse enregistreuse sans ticket de caisse.

function premier(liste: any[]): any {
  return liste[0];
  // Le retour est any : TypeScript ne vérifiera plus rien dessus.
}

Option 3 : la vraie solution, qui donne le titre à cette leçon.

Rappel : any n'est pas un type générique, c'est un abandon de type. La différence va être au cœur de toute cette leçon.

Prédis avant de lire

Si premier est typée (liste: any[]): any, que dirait le compilateur sur n.toUpperCase() si n vaut en réalité 10 ? Et si elle est typée avec <T> à la place ?

Voir la réponse

Avec any : rien. Le contrat est perdu, TypeScript ne sait pas que n est un nombre, il laisse passer toUpperCase() : le crash attendra le runtime. Avec <T> : TypeScript déduit que T = number à l'appel, et refuse toUpperCase immédiatement à la compilation. Le générique, c'est la réutilisabilité de any avec le contrat en plus.

Le moule à gâteau

Un générique, c'est une fonction (ou une interface, ou une classe) dont un des paramètres est un type. Exactement comme une fonction normale a des paramètres de valeur, un générique a un paramètre de type. On le note entre chevrons :

function premier<T>(liste: T[]): T {
  return liste[0];
}

Lis-le ainsi : « quelle que soit la forme du moule T que tu choisis, donne-moi un tableau de cette forme, et je te rends un élément de cette même forme. » Le trou dans le moule s'appelle T par convention, mais tu pourrais l'appeler Element, ou Item : ça ne change rien.

Le point fort : tu n'as pas à remplir le trou toi-même. TypeScript le déduit à l'appel :

const n = premier([10, 20, 30]);
//                 ↑ tableau de numbers
// TypeScript déduit : T = number, donc le retour est number.

const s = premier(['a', 'b', 'c']);
//                 ↑ tableau de strings
// TypeScript déduit : T = string, donc le retour est string.

Une seule fonction. Le contrat est préservé de l'entrée jusqu'à la sortie, et tout ce que tu fais avec le résultat est vérifié en conséquence.

Tu en utilisais déjà sans le savoir. number[], c'est du sucre syntaxique pour Array<number>. Array est un générique : il prend un type T, et te garantit que chaque élément est bien un T. Chaque fois que tu écris string[] ou boolean[], tu consommes un générique.

Pour démystifier complètement : un générique n'est rien d'autre qu'une fonction dont un des paramètres est un type. Pas de magie, pas de syntaxe ésotérique : juste un trou dans le moule que TypeScript remplit à l'appel.

Le moule à gâteau : la fonction premier<T> au centre reçoit [10,20,30] en haut (T devient number, sortie number) et ['a','b'] en bas (T devient string, sortie string). Même moule, contrat préservé. premier<T> trou : T rempli par inférence [10, 20, 30] T = number retour : number contrat préservé ['a', 'b'] T = string retour : string contrat préservé
Même moule, deux formes : TypeScript remplit T à chaque appel et garantit la cohérence entrée/sortie.

À toi : prouve que le moule tient

Le code ci-dessous appelle premier<T> sur un tableau de nombres. T est déduit comme number. Ensuite, il essaie d'appeler toUpperCase() sur le résultat. Clique sur Vérifier les types : tu vas voir l'erreur. C'est la preuve que le générique a tenu le contrat.

Avec any[], ce bug serait passé en silence jusqu'au runtime. Avec <T>, il est stoppé ici.

🧐 Labo TypeScript · le compilateur juge ton code (mode strict)
Bloqué sur la correction ? Voir le fix

n est un number (T déduit de [10, 20, 30]) : toUpperCase n'existe pas dessus. Remplace les deux dernières lignes par un calcul numérique :

const double: number = n * 2;
console.log(double);

Revérifie : ✓ 0 erreur. Exécute : 20. Le moule a fait son travail : T = number a été propagé jusqu'ici, et tout le code en aval est vérifié avec.

Le générique ne protège que ce que TypeScript peut vérifier au compile-time. Si tu appelles une API externe qui te renvoie n'importe quoi au runtime, le générique ne changera rien : c'est le sujet de la leçon 8.

Les contraintes : dire à T ce qu'il doit avoir

Parfois, tu veux qu'un générique reste flexible, mais pas trop. Tu as besoin que T possède au moins une certaine propriété. Pour ça, on utilise extends :

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

La contrainte T extends { length: number } dit : « T peut être n'importe quoi, à condition d'avoir une propriété length qui est un nombre. » Résultat :

plusLong('bonjour', 'oi');        // ✓ string a une length
plusLong([1, 2, 3], [4, 5]);      // ✓ tableau a une length
plusLong(42, 100);                // ✗ refusé : number n'a pas de length

La contrainte ne ferme pas le générique : elle l'affine. Tu gardes la flexibilité (string et tableau passent tous les deux), mais tu interdis ce qui n'a pas de sens.

Écrire un générique vs en consommer un. Dans le code applicatif courant (composants, services, logique métier), tu consommes des génériques, tu ne les écris presque jamais. Array<User>, Promise<Response>, Partial<Config> : tout ça, ce sont des génériques que tu utilises sans y penser. Tu écris un générique toi-même uniquement quand tu crées un utilitaire vraiment réutilisable : une fonction de tri, un hook de fetch, une classe de collection. Si tu te retrouves à écrire un générique pour un seul usage dans une seule feature, c'est souvent un signe qu'un type concret suffit.

Si ton générique n'utilise T qu'une seule fois (en entrée ou en sortie, jamais les deux), tu n'en as probablement pas besoin. Un générique vaut la peine quand T crée un lien entre entrée et sortie : « ce qui entre, c'est ce qui sort ».

The duplication dilemma

You just wrote a perfectly typed function to get the first element of a number array:

function firstNumber(list: number[]): number {
  return list[0];
}

Then the need arrives: the same thing for strings. And you're faced with three options — all ugly in their own way.

Option 1: duplicate the function. Copy-paste and change the type. Two functions for the same idea, and when the logic changes you have to remember to update both. Bad sign.

function firstNumber(list: number[]): number  { return list[0]; }
function firstString(list: string[]): string   { return list[0]; }
// …and if you need boolean? A third one?

Option 2: switch to any[]. One function, sure — but remember lesson 1: any erases the contract. The return type is any, TypeScript knows nothing about what comes out, and you lose all downstream checking. Like a cash register with no receipt.

function first(list: any[]): any {
  return list[0];
  // Return is any: TypeScript won't check anything done with it.
}

Option 3: the real solution, which gives this lesson its name.

Reminder: any is not a generic type — it's giving up on types entirely. That difference will be at the heart of this whole lesson.

Predict before reading on

If first is typed (list: any[]): any, what would the compiler say about n.toUpperCase() if n is actually 10? And if it's typed with <T> instead?

Show the answer

With any: nothing. The contract is gone, TypeScript doesn't know n is a number, it lets toUpperCase() through — the crash waits until runtime. With <T>: TypeScript infers T = number at the call site and refuses toUpperCase immediately at compile time. Generics give you the reusability of any with the contract on top.

The cake mold

A generic is a function (or interface, or class) where one of the parameters is a type. Just like a regular function has value parameters, a generic has a type parameter. It's written in angle brackets:

function first<T>(list: T[]): T {
  return list[0];
}

Read it like this: "whatever the shape of the mold T you choose, give me an array of that shape, and I'll return an element of that same shape." The hole in the mold is called T — by convention, but you could call it Element or Item: it doesn't change anything.

The key point: you don't have to fill the hole yourself. TypeScript infers it at each call:

const n = first([10, 20, 30]);
//               ↑ array of numbers
// TypeScript infers: T = number, so the return type is number.

const s = first(['a', 'b', 'c']);
//               ↑ array of strings
// TypeScript infers: T = string, so the return type is string.

One function. The contract is preserved from input through output, and everything you do with the result is checked accordingly.

You were already using one. number[] is syntactic sugar for Array<number>. Array is a generic: it takes a type T, and guarantees every element is a T. Every time you write string[] or boolean[], you're consuming a generic.

To fully demystify: a generic is nothing more than a function with a type as one of its parameters. No magic, no esoteric syntax — just a hole in the mold that TypeScript fills at each call.

The cake mold: the first<T> function at center receives [10,20,30] at the top (T becomes number, output is number) and ['a','b'] at the bottom (T becomes string, output is string). Same mold, contract preserved. first<T> hole: T filled by inference [10, 20, 30] T = number return: number contract preserved ['a', 'b'] T = string return: string contract preserved
Same mold, two shapes: TypeScript fills T at each call and guarantees input/output consistency.

Your turn: prove the mold holds

The code below calls first<T> on a number array. T is inferred as number. Then it tries to call toUpperCase() on the result. Click Check the types: you'll see the error. That's proof the generic kept the contract.

With any[], this bug would have passed silently until runtime. With <T>, it's stopped right here.

🧐 TypeScript lab · the compiler judges your code (strict mode)
Stuck on the fix? Show it

n is a number (T inferred from [10, 20, 30]): toUpperCase doesn't exist on it. Replace the last two lines with a numeric computation:

const double: number = n * 2;
console.log(double);

Re-check: ✓ 0 errors. Run: 20. The mold did its job: T = number was propagated all the way here, and all downstream code is checked accordingly.

Beware: the generic only protects what TypeScript can verify at compile time. If you call an external API that sends you anything at runtime, the generic won't help — that's the topic of lesson 8.

Constraints: telling T what it must have

Sometimes you want a generic to stay flexible, but not too flexible. You need T to have at least a certain property. For that, we use extends:

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

The constraint T extends { length: number } says: "T can be anything, as long as it has a length property that is a number." Result:

longest('hello', 'hi');           // ✓ string has a length
longest([1, 2, 3], [4, 5]);       // ✓ array has a length
longest(42, 100);                 // ✗ rejected: number has no length

The constraint doesn't close the generic: it refines it. You keep the flexibility (string and array both work), but you block what doesn't make sense.

Writing a generic vs consuming one. In everyday application code (components, services, business logic), you consume generics — you almost never write them. Array<User>, Promise<Response>, Partial<Config>: all generics you use without thinking. You write a generic yourself only when you're building a truly reusable utility: a sort function, a fetch hook, a collection class. If you're writing a generic for a single use in a single feature, a concrete type is usually enough.

If your generic uses T only once (either in input or output, never both), you probably don't need it. A generic earns its place when T creates a link between input and output — "what comes in is what comes out."

🎯 Pratique

S'entraîner (clique pour ouvrir) :

💬 Ré-explique sans regarder
Ré-explique sans regarder

Explique un générique avec l'image du moule à gâteau, et pourquoi c'est mieux que any.

Une bonne explication dit : <T> est un paramètre de type, un trou dans le moule ; TypeScript le remplit par inférence à chaque appel (pas besoin de l'écrire à la main) ; le contrat entrée/sortie est préservé et vérifié en aval (T = number → tout ce qu'on fait avec le résultat est vérifié comme un number) ; any offre la réutilisabilité mais efface le contrat : le bug toUpperCase du labo serait passé en silence.
🧠 Rappel libre
Rappel libre

Sans remonter : cite un générique que tu utilisais déjà sans le savoir, et quelle est la valeur de T dans premier([1, 2]) ?

number[] est du sucre pour Array<number> : Array est un générique, T = number dans ce cas. Dans premier([1, 2]), TypeScript déduit T = number du tableau : le retour est donc un number, vérifié partout ensuite.
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

Tu as une fonction générique qui génère une erreur de compilation. L'IA propose : « Pour simplifier, j'ai remplacé <T> par any : ça revient au même et l'erreur disparaît. » Tu acceptes, ou tu rejettes ?

À rejeter. Ça ne revient pas au même : any efface le contrat de type en aval. Le bug du labo (toUpperCase sur un number) passerait en silence avec any et exploserait au runtime. Si le générique génère une erreur, c'est souvent qu'il manque une contrainte (extends) ou que le type concret attendu n'est pas encore inféré correctement. La réponse, c'est affiner le générique, pas l'effacer.
Qu'est-ce que <T> dans function premier<T>(liste: T[]): T ?
Que vaut T dans premier(['a', 'b']) ?
Que fait <T extends { length: number }> ?
Générique vs any pour une fonction réutilisable : la vraie différence ?
Next step

The mold is set. In lesson 7, we discover utility types: Partial, Pick, Omit — ready-made generics to transform existing types instead of rewriting them from scratch.

Lesson 7: Utility types →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement