Les generics TypeScript sont l'une des fonctionnalites les plus puissantes du systeme de types. Ils vous permettent d'ecrire du code flexible, reutilisable et type-safe en parametrant les types. Au lieu de dupliquer des fonctions ou des classes pour chaque type de donnees, les generics vous permettent d'ecrire une seule fois et d'utiliser avec n'importe quel type tout en maintenant une securite de type complete. Ce guide couvre tout, des bases aux patterns avances.
Votre Premiere Fonction Generique
Le point de depart classique est la fonction identite — une fonction qui retourne exactement ce qu'elle recoit. Sans generics, vous perdriez les informations de type en utilisant any, ou vous devriez ecrire des fonctions separees pour chaque type.
Avec les generics, vous definissez un parametre de type (conventionnellement nomme T) qui agit comme un espace reservee. Lorsque vous appelez la fonction, TypeScript infere automatiquement le type reel.
// Without generics — loses type info
function identityAny(value: any): any {
return value;
}
const result1 = identityAny("hello"); // type: any (not string!)
// With generics — preserves type info
function identity<T>(value: T): T {
return value;
}
const result2 = identity("hello"); // type: string ✓
const result3 = identity(42); // type: number ✓
const result4 = identity(true); // type: boolean ✓
// Explicit type argument (rarely needed)
const result5 = identity<string>("hello"); // type: stringFonctions Generiques
Les fonctions generiques peuvent accepter plusieurs parametres de type et meme avoir des types par defaut. Cela les rend incroyablement polyvalentes pour construire des fonctions utilitaires.
// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p = pair("hello", 42); // type: [string, number]
// Swap function with two type params
function swap<A, B>(tuple: [A, B]): [B, A] {
return [tuple[1], tuple[0]];
}
const swapped = swap(["hello", 42]); // type: [number, string]
// Default type parameters
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const strings = createArray(3, "x"); // T inferred as string
const numbers = createArray(3, 0); // T inferred as number
const defaults = createArray<>(3, ""); // T defaults to stringLes parametres de type par defaut fonctionnent comme les parametres de fonction par defaut — si l'appelant ne specifie pas de type, le type par defaut est utilise.
Interfaces et Types Generiques
Les generics ne se limitent pas aux fonctions. Vous pouvez parametrer des interfaces et des alias de type pour creer des structures de donnees flexibles.
// Generic interface
interface Box<T> {
value: T;
label: string;
}
const stringBox: Box<string> = { value: "hello", label: "greeting" };
const numberBox: Box<number> = { value: 42, label: "answer" };
// Generic interface with multiple params
interface KeyValuePair<K, V> {
key: K;
value: V;
}
// Generic type alias
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
const ok: Result<string> = { success: true, data: "hello" };
const fail: Result<string> = { success: false, error: new Error("oops") };
// Generic type for callbacks
type AsyncCallback<T> = (error: Error | null, data: T | null) => void;
// Generic type for nullable values
type Nullable<T> = T | null | undefined;Les interfaces generiques sont particulierement utiles pour definir la forme des reponses API, des collections et des conteneurs de donnees.
Classes Generiques
Les classes peuvent aussi etre generiques. C'est utile pour construire des structures de donnees comme les piles, files d'attente, listes chainees, ou tout conteneur devant fonctionner avec plusieurs types.
// Generic Stack class
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // type: number | undefined
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.peek(); // type: string | undefined
// Generic class with multiple type params
class HashMap<K, V> {
private map = new Map<K, V>();
set(key: K, value: V): void {
this.map.set(key, value);
}
get(key: K): V | undefined {
return this.map.get(key);
}
has(key: K): boolean {
return this.map.has(key);
}
}Le parametre de type est disponible dans toute la classe — dans les proprietes, parametres de methode et types de retour.
Contraintes Generiques avec extends
Les generics sans contrainte acceptent n'importe quel type, mais parfois vous devez restreindre les types autorises. Le mot-cle extends permet d'ajouter des contraintes.
Contraindre aux Cles d'Objet avec keyof
Un pattern tres courant est de contraindre un parametre de type a etre une cle valide d'un autre type. Cela garantit un acces aux proprietes type-safe.
// getProperty with keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, email: "alice@example.com" };
getProperty(user, "name"); // type: string ✓
getProperty(user, "age"); // type: number ✓
// getProperty(user, "foo"); // Error: "foo" is not assignable to "name" | "age" | "email"
// Pick specific keys from an object
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => { result[key] = obj[key]; });
return result;
}
const subset = pick(user, ["name", "email"]);
// type: { name: string; email: string }Contraindre a des Formes Specifiques
Vous pouvez exiger qu'un parametre de type possede certaines proprietes en etendant une interface ou un type objet.
// Constraint to objects with a length property
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): T {
console.log(`Length: ${value.length}`);
return value;
}
logLength("hello"); // OK — string has length
logLength([1, 2, 3]); // OK — array has length
logLength({ length: 5, name: "test" }); // OK — has length
// logLength(42); // Error — number has no length property
// Constraint to objects with an id
interface HasId {
id: number | string;
}
function findById<T extends HasId>(items: T[], id: T["id"]): T | undefined {
return items.find(item => item.id === id);
}
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
const found = findById(users, 1);
// type: { id: number; name: string } | undefinedTypes Conditionnels
Les types conditionnels permettent de choisir entre deux types selon une condition, avec la syntaxe T extends U ? X : Y. Ils sont la base de nombreux calculs avances au niveau des types.
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true
// Flatten arrays, leave other types alone
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type D = Flatten<string[]>; // string
type E = Flatten<number[][]>; // number[]
type F = Flatten<string>; // string (not an array, returned as-is)
// Extract return type (simplified ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type G = MyReturnType<() => string>; // string
type H = MyReturnType<(x: number) => boolean>; // boolean
// Extract promise value
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type I = UnwrapPromise<Promise<string>>; // string
type J = UnwrapPromise<number>; // numberLes types conditionnels deviennent particulierement puissants combines avec infer, qui permet d'extraire des types depuis d'autres types.
Types Mappes
Les types mappes transforment les proprietes d'un type existant pour creer un nouveau type. Ils iterent sur les cles avec la syntaxe [K in keyof T].
// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties required (like Required<T>)
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Transform property types
type Stringify<T> = {
[K in keyof T]: string;
};
interface User {
name: string;
age: number;
active: boolean;
}
type StringifiedUser = Stringify<User>;
// { name: string; age: string; active: string }
// Mapped type with conditional
type OptionalNullable<T> = {
[K in keyof T]: undefined extends T[K] ? T[K] | null : T[K];
};Vous pouvez ajouter ou supprimer des modificateurs comme readonly et ? dans les types mappes.
Types Utilitaires Integres
TypeScript inclut de nombreux types utilitaires construits avec des generics. Voici les plus couramment utilises :
| Type Utilitaire | Description | Exemple |
|---|---|---|
Partial<T> | Rend toutes les proprietes optionnelles | Partial<User> |
Required<T> | Rend toutes les proprietes obligatoires | Required<Partial<User>> |
Pick<T, K> | Selectionne les proprietes specifiees | Pick<User, "name" | "email"> |
Omit<T, K> | Exclut les proprietes specifiees | Omit<User, "password"> |
Record<K, V> | Cree un mappage cle-valeur type | Record<string, number> |
Readonly<T> | Rend toutes les proprietes en lecture seule | Readonly<Config> |
ReturnType<T> | Extrait le type de retour d'une fonction | ReturnType<typeof fn> |
Parameters<T> | Extrait le tuple des types de parametres | Parameters<typeof fn> |
// Utility types in action
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// For update forms — all fields optional
type UpdateUser = Partial<User>;
// Public user — omit password
type PublicUser = Omit<User, "password">;
// User creation — omit auto-generated fields
type CreateUser = Omit<User, "id" | "createdAt">;
// Lookup table
type UserLookup = Record<number, User>;
// Immutable user from API
type FrozenUser = Readonly<User>;
// Extract function types
function fetchUser(id: number): Promise<User> { /* ... */ }
type FetchReturn = ReturnType<typeof fetchUser>; // Promise<User>
type FetchParams = Parameters<typeof fetchUser>; // [number]Patterns du Monde Reel
Wrapper de Reponse API Generique
L'une des utilisations les plus courantes des generics est d'envelopper les reponses API dans une structure coherente.
// Generic API response wrapper
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
};
}
// Usage with specific types
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
return res.json();
}
// TypeScript knows the exact data type
const userRes = await fetchApi<User>("/api/users/1");
console.log(userRes.data.name); // type: string ✓
const productsRes = await fetchApi<Product[]>("/api/products");
console.log(productsRes.data[0].price); // type: number ✓Props de Composant React Generique
Les generics permettent de construire des composants React fonctionnant avec differents types tout en maintenant la securite de type sur les props.
// Generic list component props
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
onItemClick?: (item: T) => void;
emptyMessage?: string;
}
function GenericList<T>({
items,
renderItem,
keyExtractor,
onItemClick,
emptyMessage = "No items",
}: ListProps<T>) {
if (items.length === 0) return <p>{emptyMessage}</p>;
return (
<ul>
{items.map((item, index) => (
<li
key={keyExtractor(item)}
onClick={() => onItemClick?.(item)}
>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// Usage — TypeScript infers T from items
<GenericList
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => <span>{user.name}</span>}
onItemClick={(user) => console.log(user.email)}
/>Pattern Repository Generique
Le pattern repository abstrait l'acces aux donnees derriere une interface generique, permettant de changer de source de donnees sans modifier la logique metier.
// Generic repository interface
interface Entity {
id: number | string;
}
interface Repository<T extends Entity> {
findAll(): Promise<T[]>;
findById(id: T["id"]): Promise<T | null>;
create(data: Omit<T, "id">): Promise<T>;
update(id: T["id"], data: Partial<T>): Promise<T>;
delete(id: T["id"]): Promise<boolean>;
}
// Concrete implementation
class ApiRepository<T extends Entity> implements Repository<T> {
constructor(private baseUrl: string) {}
async findAll(): Promise<T[]> {
const res = await fetch(this.baseUrl);
return res.json();
}
async findById(id: T["id"]): Promise<T | null> {
const res = await fetch(`${this.baseUrl}/${id}`);
if (!res.ok) return null;
return res.json();
}
async create(data: Omit<T, "id">): Promise<T> {
const res = await fetch(this.baseUrl, {
method: "POST",
body: JSON.stringify(data),
});
return res.json();
}
async update(id: T["id"], data: Partial<T>): Promise<T> {
const res = await fetch(`${this.baseUrl}/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
return res.json();
}
async delete(id: T["id"]): Promise<boolean> {
const res = await fetch(`${this.baseUrl}/${id}`, { method: "DELETE" });
return res.ok;
}
}
// Usage
interface User extends Entity { id: number; name: string; email: string; }
const userRepo = new ApiRepository<User>("/api/users");
const users = await userRepo.findAll(); // type: User[]Emetteur d'Evenements Generique
Un emetteur d'evenements type-safe garantit que les noms d'evenements et leurs charges utiles sont correctement types.
// Type-safe event emitter
type EventMap = Record<string, any>;
type EventCallback<T> = (payload: T) => void;
class TypedEmitter<Events extends EventMap> {
private listeners: {
[K in keyof Events]?: EventCallback<Events[K]>[];
} = {};
on<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
off<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
const cbs = this.listeners[event];
if (cbs) {
this.listeners[event] = cbs.filter(cb => cb !== callback);
}
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
const cbs = this.listeners[event];
if (cbs) {
cbs.forEach(cb => cb(payload));
}
}
}
// Define your event types
interface AppEvents {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"item:added": { itemId: string; quantity: number };
"error": { message: string; code: number };
}
const emitter = new TypedEmitter<AppEvents>();
// Fully typed — payload type is inferred
emitter.on("user:login", (payload) => {
console.log(payload.userId); // type: string ✓
console.log(payload.timestamp); // type: number ✓
});
// Error: "unknown:event" is not assignable to keyof AppEvents
// emitter.on("unknown:event", () => {});Erreurs Courantes
Voici les erreurs les plus frequentes avec les generics et leurs corrections :
| Erreur | Probleme | Correction |
|---|---|---|
| Utiliser any au lieu des generics | Perte de toutes les informations de type | Utiliser un parametre de type T au lieu de any |
| Sur-contraindre les parametres de type | Rend le generique inutilisable pour des types valides | Ne contraindre que ce dont vous avez besoin |
| Parametres de type inutiles | Ajouter T utilise une seule fois ajoute de la complexite | Si T apparait seulement dans le type de retour, utiliser le type concret |
| Ne pas utiliser de contraintes lors de l'acces aux proprietes | Erreur TypeScript : la propriete n'existe pas sur le type T | Ajouter une contrainte extends : T extends { prop: type } |
| Confondre les valeurs par defaut et les contraintes | Le type par defaut ne restreint pas — les appelants peuvent passer d'autres types | Utiliser extends pour restreindre, = pour le defaut |
| Ne pas inferer quand c'est possible | Passer explicitement les types quand TS peut les inferer | Laisser TypeScript inferer les types depuis les arguments |
| Utiliser T[] au lieu de readonly T[] | Permet la mutation de tableaux qui devraient etre en lecture seule | Utiliser readonly T[] pour les donnees immuables des APIs |
// ❌ Mistake: Using any
function badParse(json: string): any {
return JSON.parse(json);
}
const data = badParse('{"name":"Alice"}');
data.whatever; // No error — but might crash at runtime!
// ✓ Correction: Using generics with validation
function safeParse<T>(json: string, validator: (data: unknown) => data is T): T | null {
try {
const parsed = JSON.parse(json);
return validator(parsed) ? parsed : null;
} catch {
return null;
}
}
// ❌ Mistake: Unnecessary type parameter
function badLength<T>(arr: T[]): number {
return arr.length; // T is never meaningfully used
}
// ✓ Correction: Just use the concrete type
function goodLength(arr: unknown[]): number {
return arr.length;
}
// ❌ Mistake: Missing constraint
function badGetName<T>(obj: T): string {
return obj.name; // Error: Property 'name' does not exist on type 'T'
}
// ✓ Correction: Add constraint
function goodGetName<T extends { name: string }>(obj: T): string {
return obj.name; // OK ✓
}Questions Frequemment Posees
Quelle est la difference entre les generics et le type any ?
Les generics preservent les informations de type tandis que any les supprime. Avec les generics, si vous passez une chaine, TypeScript sait que le retour est aussi une chaine. Avec any, toutes les informations de type sont perdues.
Quand utiliser les generics vs les surcharges de fonctions ?
Utilisez les generics quand la logique est identique pour tous les types. Utilisez les surcharges quand l'implementation differe selon les types d'entree.
Peut-on utiliser les generics avec les fonctions flechees dans les fichiers TSX ?
Oui, mais ajoutez une virgule finale pour eviter l'ambiguite JSX : const fn = <T,>(arg: T): T => arg.
Combien de parametres de type un generique devrait-il avoir ?
Le moins possible. Un ou deux est courant. Trois est parfois necessaire. Au-dela, envisagez un refactoring.
Les generics affectent-ils les performances a l'execution ?
Non. Les generics sont purement une fonctionnalite de compilation. Ils sont completement effaces et ne generent aucun surcout a l'execution.
Quelle est la convention de nommage des parametres de type ?
Les lettres majuscules uniques sont conventionnelles : T pour type, K pour cle, V pour valeur, E pour element, R pour type de retour. Pour les generics complexes, des noms descriptifs comme TData sont acceptables.