TypeScript — Les Generics

Écrire du code réutilisable SANS perdre le typage

Utilisez les flèches, cliquez ou glissez pour naviguer

Objectifs de la leçon

1. Comprendre le problème que les generics résolvent

Duplication de code quand on veut la même logique pour plusieurs types

2. Écrire une fonction générique identity<T>

Un seul code, tous les types

3. Utiliser les generics sur les interfaces

Box<T>, ApiResponse<T>

4. Savoir quand utiliser un generic vs un type concret

Le bon outil au bon moment

Plan du cours

1

Montrer le problème

3 fonctions identiques pour 3 types différents — c'est de la duplication

2

any : la fausse solution

On perd le typage — puis introduire <T> comme vraie solution

3

Live coding : fonctions génériques

identity<T>, first<T>, makePair<T> — la magie de l'inférence

4

Generics sur les interfaces

Box<T>, ApiResponse<T> — un seul type, réutilisable partout

5

Exercice live : map<T, U>

Deux paramètres de type — transformer un type en un autre

Le problème : 3 fonctions identiques

Même logique, 3 types différents → 3 fonctions copiées-collées

function identityNumber(value: number): number {

return value;

}

function identityString(value: string): string {

return value;

}

function identityBoolean(value: boolean): boolean {

return value;

}

// 3 fonctions qui font EXACTEMENT la même chose! ❌

⚠️ Et si on veut supporter un 4e type? On copie-colle encore?

Pourquoi c'est mauvais

1️⃣

Duplication

3 fois le même code

2️⃣

Maintenance

Un bug = 3 endroits à corriger

3️⃣

Incomplet

Nouveau type = nouvelle copie

// Imaginons : on veut ajouter un log dans chaque fonction

function identityNumber(value: number): number {

console.log("received:", value); // ← à ajouter dans les 3 fonctions!

return value;

}

🎯 Copier-coller = dette technique. On veut UNE seule fonction.

Ce qu'on veut

Une seule fonction qui marche pour TOUT type

Une seule implémentation

Le type est préservé — pas de perte d'information

Réutilisable — même pour les types qu'on ne connaît pas encore

// Le rêve :

function identity<T>(value: T): T {

return value;

}

La fausse solution : any

Une seule fonction avec any — ça compile, mais...

function identity(value: any): any {

return value;

}

✅ Une seule fonction

Plus de duplication

❌ On perd le typage

TypeScript ne vérifie plus rien

any en action : le danger

Avec any, TypeScript laisse passer N'IMPORTE QUOI

function identity(value: any): any {

return value;

}

const result = identity(5);

// TypeScript laisse passer ça :

result.toUpperCase(); // ❌ Compile! Mais crash à l'exécution!

result.map(x => x); // ❌ Compile! Mais 5 n'est pas un tableau!

🎯 any = désactiver TypeScript. C'est comme enlever les freins d'une voiture.

La vraie solution : <T>

Un paramètre de type — comme un paramètre de fonction, mais pour les types

function identity<T>(value: T): T {

return value;

}

<T>

Paramètre de type

Un placeholder

value: T

Paramètre typé

T sera remplacé

: T

Retour typé

Même type que l'entrée

any vs <T> : comparaison

❌ any

function identity(value: any): any {

return value;

}

• TypeScript ne vérifie rien

• result.toUpperCase() compile

• Erreurs découvertes à l'exécution

✅ <T>

function identity<T>(value: T): T {

return value;

}

• TypeScript préserve le type

• result.toUpperCase() → erreur!

• Erreurs attrapées AVANT l'exécution

const result = identity(5); // T est inféré comme number

result.toUpperCase(); // ❌ Erreur TS! number n'a pas toUpperCase

🎯 <T> préserve l'info de type — any la détruit

identity<T> — pas à pas

1️⃣ Déclarer la fonction avec un paramètre de type

function identity<T>(value: T): T {

return value;

}

2️⃣ Appeler en spécifiant le type explicitement

identity<string>("hello"); // T = string

identity<number>(42); // T = number

3️⃣ Laisser TypeScript inférer T automatiquement

identity("hello"); // T = string — automatique!

identity(42); // T = number — automatique!

L'inférence de TypeScript

TypeScript devine T tout seul — pas besoin de le spécifier!

Appel explicite

identity<string>("hi");

// Verbeux mais clair

Appel implicite (inférence)

identity("hi");

// TypeScript comprend que T = string

const a = identity("hello"); // a: string

const b = identity(42); // b: number

const c = identity(true); // c: boolean

// Chaque appel a le bon type — sans rien spécifier!

💡 Règle pratique : ne spécifiez <T> que si TypeScript ne peut pas inférer tout seul

first<T> — premier élément d'un tableau

function first<T>(arr: T[]): T | undefined {

return arr[0];

}

<T>

Le type des éléments

T[]

Un tableau de T

T | undefined

Le premier ou rien

first([1, 2, 3]); // number | undefined → 1

first(["a", "b"]); // string | undefined → "a"

first([]); // undefined

makePair<T> — créer un tuple

function makePair<T>(a: T, b: T): [T, T] {

return [a, b];

}

Les deux paramètres doivent être du même type T

makePair(1, 2); // [number, number]

makePair("a", "b"); // [string, string]

// makePair(1, "b"); // ❌ Erreur! number ≠ string

💡 T est le même partout dans la fonction — c'est ça la garantie!

Conventions de nommage

T est une convention, pas une obligation — mais tout le monde l'utilise

T

Type

Le type principal, le plus courant

identity<T>(value: T): T

K

Key

Pour les clés d'un objet

getProp<K>(key: K)

V

Value

Pour les valeurs associées

Map<K, V>

E

Element

Pour les éléments d'une collection

Array<E>

// Noms explicites aussi possibles :

function merge<TInput, TOutput>(...) { ... }

Box<T>

Une boîte qui contient n'importe quel type

interface Box<T> {

value: T;

}

T = le type de ce qui est DANS la boîte

Box<string> → une boîte de string

Box<number> → une boîte de number

Box<T> en pratique

interface Box<T> {

value: T;

}

const stringBox: Box<string> = { value: "hello" };

const numberBox: Box<number> = { value: 42 };

✅ Type correct

stringBox.value.toUpperCase();

❌ Erreur TypeScript

numberBox.value.toUpperCase();

// number n'a pas toUpperCase!

❌ Erreur : type mismatch

const badBox: Box<string> = { value: 42 };

// Type 'number' is not assignable to 'string'

ApiResponse<T>

Un cas réel : chaque API retourne la même structure, mais des données différentes

interface ApiResponse<T> {

data: T;

status: number;

message: string;

}

// Réponse avec un utilisateur

type UserResponse = ApiResponse<User>;

// Réponse avec une liste de produits

type ProductsResponse = ApiResponse<Product[]>;

💡 Un seul type ApiResponse, réutilisable pour TOUTE les réponses API!

ApiResponse<T> en pratique

interface User { name: string; age: number; }

interface Product { title: string; price: number; }

ApiResponse<User>

{

data: { name: "Alice", age: 25 },

status: 200,

message: "OK"

}

data.name ✅ auto-complété!

ApiResponse<Product[]>

{

data: [{ title: "Book", price: 10 }],

status: 200,

message: "OK"

}

data[0].price ✅ auto-complété!

🎯 Un seul type, réutilisable partout — data a toujours le bon type!

Generic vs Type concret

✅ Utiliser un generic quand

• La même logique s'applique à plusieurs types

• Tu veux préserver l'info de type

• Le type est déterminé par l'appelant

function first<T>(arr: T[]): T

// Marche pour string[], number[], ...

✅ Utiliser un type concret quand

• Tu connais le type à l'avance

• La fonction ne marche que pour un type

• Pas besoin de flexibilité

function double(n: number): number

// Ne fait sens que pour number

🎯 Pas de generic "au cas où" — un generic doit servir à AU MOINS 2 types

Deux paramètres de type

<T, U> — l'entrée et la sortie peuvent être différents

T

Type d'entrée

U

Type de sortie

// T = type des éléments en entrée

// U = type des éléments en sortie

function map<T, U>(...): U[]

map<T, U> — construit ensemble

1️⃣ La signature

function map<T, U>(arr: T[], fn: (item: T) => U): U[]

2️⃣ L'implémentation

function map<T, U>(arr: T[], fn: (item: T) => U): U[] {

const result: U[] = [];

for (const item of arr) {

result.push(fn(item));

}

return result;

}

3️⃣ L'appel

map([1, 2, 3], n => n.toString());

// T = number, U = string → string[]

map<T, U> en action

// number → string

map([1, 2, 3], n => n.toString());

// → ["1", "2", "3"] (T=number, U=string)

// string → number

map(["1", "2"], s => parseInt(s));

// → [1, 2] (T=string, U=number)

// User → string

map(users, u => u.name);

// → ["Alice", "Bob"] (T=User, U=string)

💡 TypeScript infère T et U automatiquement — pas besoin de spécifier map<number, string>(...)

⚠️ Pièges courants

Piège 1 : Confondre <T> avec un type concret

T est un type qui existe

❌ Non! T est un placeholder

T est remplacé à l'appel

✅ Comme un paramètre de fonction

Piège 2 : La syntaxe <T> intimide

Rassurer : <T> c'est juste un paramètre, comme (x: number) mais pour les types

// Paramètre de fonction : la valeur change à chaque appel

function f(x: number) { ... }

// Paramètre de type : le TYPE change à chaque appel

function g<T>(x: T) { ... }

Piège 3 : Aller trop vite sur <T, U>

Commencer par un seul T. <T, U> vient naturellement quand on a besoin de transformer un type en un autre.

À retenir !

<T> = placeholder

Un paramètre de type — comme un paramètre de fonction, mais pour les types

Inférence automatique

TypeScript devine T — pas besoin de le spécifier à chaque appel

Réutilisable SANS perdre le typage

Une seule implémentation, tous les types, zéro any

Conventions

T (Type), K (Key), V (Value), E (Element)

Un generic = un paramètre pour les types

Comme (x: number) prend une valeur, <T> prend un type

Questions ?

Les generics = écrire une fois, utiliser pour tous les types

function identity<T>(value: T): T { return value; }