TypeScript Generics

Contraintes · Generics multiples · Types utilitaires

Partial · Pick · Omit · Record · keyof · extends

Objectifs de la leçon

1. Contraintes generics

Utiliser <T extends ...> pour limiter T

2. Generics multiples

Combiner plusieurs paramĂštres <T, U>

3. Types utilitaires

MaĂźtriser Partial, Required, Pick, Omit

4. Record<K, V>

Typer les dictionnaires et les objets dynamiques

Plan du cours

1. Recap J1

Generics de base : identity<T>, Box<T>

2. Contraintes

T est trop libre — <T extends HasLength>

3. keyof & generics multiples

Extraire les clés d'un type, getProperty<T, K>

4. Types utilitaires

Partial, Pick, Omit, Record

5. Exercice en live

Combiner Partial<Omit<User, 'id'>> pour un DTO

Module 1

Recap : Generics de base

Ce qu'on a vu la derniĂšre fois

Rappel : identity<T> et Box<T>

Un generic = un paramĂštre de type. Comme un argument, mais pour les types.

// identity : retourne la mĂȘme valeur avec le bon type
function identity<T>(value: T): T {
  return value;
}

const num = identity(42);        // type: number
const str = identity("hello");   // type: string

// Box : un conteneur générique
interface Box<T> {
  value: T;
  label: string;
}

const numberBox: Box<number> = { value: 42, label: "age" };
const stringBox: Box<string> = { value: "hello", label: "greeting" };

💡 Rappel

T est une convention. On peut utiliser n'importe quel nom, mais T (Type), U, K (Key), V (Value) sont standards.

Le problĂšme : T est trop libre

Sans contrainte, T peut ĂȘtre absolument n'importe quoi.

// On veut une fonction qui retourne la longueur de quelque chose
function getLength<T>(item: T): number {
  return item.length; // ❌ ERREUR : Property 'length' does not exist on type 'T'
}

⚠ Pourquoi cette erreur ?

TypeScript ne sait pas si T a une propriété .length.

T pourrait ĂȘtre number, boolean, null
 aucun n'a .length

✓ Solution

On doit contraindre T pour garantir qu'il possĂšde .length

Module 2

Contraintes : <T extends ...>

Limiter ce que T peut ĂȘtre

extends = contrainte, pas héritage

Dans les generics, extends signifie "doit avoir au moins..."

✗ Ce n'est PAS ça

// Héritage de classe

class Dog extends Animal {}

// Dog hérite de Animal

// = Dog EST un Animal

✓ C'est ça

// Contrainte de type

<T extends HasLength>

// T doit satisfaire HasLength

// = T A au moins .length

🧠 MnĂ©monique

Dans les generics : extends = "est compatible avec" / "a au moins les propriétés de"

<T extends HasLength> en action

// Définir la contrainte
interface HasLength {
  length: number;
}

// Appliquer la contrainte sur T
function getLength<T extends HasLength>(item: T): number {
  return item.length; // ✅ OK : TypeScript sait que T a .length
}

// ✅ Fonctionne — string a .length
getLength("hello");         // 5

// ✅ Fonctionne — Array a .length
getLength([1, 2, 3]);       // 3

// ✅ Fonctionne — objet avec .length
getLength({ length: 10, name: "test" });  // 10

// ❌ ERREUR — number n'a pas .length
getLength(42);
// Argument of type 'number' is not assignable
// to parameter of type 'HasLength'

Contrainte avec un type inline

Pas besoin de créer une interface séparée pour les cas simples.

// Contrainte inline — T doit avoir au moins { id: number }
function printId<T extends { id: number }>(item: T): void {
  console.log(`ID: ${item.id}`);
}

printId({ id: 1, name: "Alice" });    // ✅ OK
printId({ id: 2, email: "b@c.com" }); // ✅ OK
printId({ name: "Bob" });             // ❌ ERREUR : pas de .id

// Contrainte avec un type plus complexe
function merge<T extends object>(target: T, source: Partial<T>): T {
  return { ...target, ...source };
}

const user = merge(
  { name: "Alice", age: 30 },
  { age: 31 }
); // ✅ type: { name: string; age: number }

Contraintes courantes

Contrainte Signification Exemple
T extends object T est un objet (pas primitif) Exclut string, number, boolean
T extends string T est un string ou un literal "hello" | "world"
T extends { id: number } T a au moins une propriété id User, Product, Order...
T extends unknown[] T est un tableau string[], number[], any[]
T extends (...args: any[]) => any T est une fonction Callbacks, handlers

Module 3

keyof & Generics multiples

Extraire les clés d'un type et combiner <T, K>

keyof — L'union des clĂ©s d'un type

keyof T donne toutes les clés possibles de T sous forme d'union.

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

// keyof User = "id" | "name" | "email" | "age"
type UserKeys = keyof User;

// On peut l'utiliser pour restreindre une variable
let key: keyof User;
key = "name";   // ✅
key = "email";  // ✅
key = "phone";  // ❌ ERREUR : "phone" n'existe pas dans User

💡 Astuce VS Code

Hover sur keyof User dans VS Code pour voir l'union complÚte des clés.

Generics multiples : <T, K>

On peut avoir plusieurs paramĂštres de type, chacun avec sa propre contrainte.

// K est contraint Ă  ĂȘtre une clĂ© de T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Alice", age: 30 };

const name = getProperty(user, "name");   // type: string ✅
const age = getProperty(user, "age");     // type: number ✅
const oops = getProperty(user, "phone");  // ❌ ERREUR !
// Argument of type '"phone"' is not assignable
// to parameter of type '"id" | "name" | "age"'

🔑 DĂ©composition

T = le type de l'objet · K extends keyof T = K est une clé valide de T · T[K] = le type de la valeur à cette clé

T[K] — Indexed Access Types

Accéder au type d'une propriété dynamiquement.

interface User {
  id: number;
  name: string;
  email: string;
  isAdmin: boolean;
}

// AccÚs à un type spécifique
type UserId = User["id"];          // number
type UserName = User["name"];      // string
type UserAdmin = User["isAdmin"];  // boolean

// AccÚs avec une union de clés
type UserStrings = User["name" | "email"];  // string

// Avec keyof — toutes les valeurs possibles
type UserValues = User[keyof User]; // number | string | boolean

C'est exactement ce que fait T[K] dans getProperty : le type de retour dépend de la clé passée.

Exemple avancé : setProperty<T, K>

function setProperty<T, K extends keyof T>(
  obj: T,
  key: K,
  value: T[K]  // La valeur doit correspondre au type de la clé
): T {
  return { ...obj, [key]: value };
}

const user = { id: 1, name: "Alice", age: 30 };

// ✅ Correct — "name" attend un string
setProperty(user, "name", "Bob");

// ✅ Correct — "age" attend un number
setProperty(user, "age", 31);

// ❌ ERREUR — "age" attend un number, pas un string
setProperty(user, "age", "thirty-one");
// Argument of type 'string' is not assignable
// to parameter of type 'number'

Generics multiples indépendants

Quand T et U n'ont pas de lien entre eux.

// Combiner deux valeurs de types différents
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const p1 = pair("hello", 42);       // type: [string, number]
const p2 = pair(true, [1, 2, 3]);   // type: [boolean, number[]]

// Convertir un type en un autre
function transform<T, U>(
  input: T,
  transformer: (value: T) => U
): U {
  return transformer(input);
}

const length = transform("hello", (s) => s.length);  // type: number
const upper = transform("hello", (s) => s.toUpperCase()); // type: string

Module 4

Types utilitaires

Partial · Required · Pick · Omit · Record

Vue d'ensemble des types utilitaires

Des outils intégrés à TypeScript pour transformer les types.

Utilitaire Ce qu'il fait Cas d'usage
Partial<T> Rend toutes les propriétés optionnelles Formulaires, mises à jour partielles
Required<T> Rend toutes les propriétés obligatoires Validation, données complÚtes
Pick<T, K> Garde seulement les clés K DTOs, réponses API partielles
Omit<T, K> Supprime les clés K Exclure id, createdAt, etc.
Record<K, V> Objet avec clés K et valeurs V Dictionnaires, lookup maps

Partial<T> — Tout devient optionnel

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

// Partial<User> équivaut à :
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   age?: number;
// }

// Use case : mise Ă  jour partielle
function updateUser(id: number, updates: Partial<User>): User {
  const current = getUserById(id);
  return { ...current, ...updates };
}

// ✅ On peut ne passer que ce qui change
updateUser(1, { name: "Bob" });
updateUser(1, { age: 31, email: "new@mail.com" });
updateUser(1, {}); // ✅ MĂȘme un objet vide est valide

Comment Partial fonctionne sous le capot

C'est un mapped type — il itĂšre sur les clĂ©s et ajoute ?

// L'implémentation réelle de Partial dans TypeScript :
type Partial<T> = {
  [K in keyof T]?: T[K];
};

// Décomposition :
// 1. keyof T        → union de toutes les clĂ©s ("id" | "name" | "email" | "age")
// 2. [K in ...]     → pour chaque clĂ© K
// 3. ?              → rendre optionnel
// 4. T[K]           → garder le type original de la valeur

💡 Pas besoin de mĂ©moriser l'implĂ©mentation

Mais comprendre le mécanisme aide à créer ses propres types utilitaires plus tard.

Pick<T, K> — Garder seulement certaines clĂ©s

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  password: string;
  createdAt: Date;
}

// Garder seulement id, name, email
type UserPublicInfo = Pick<User, "id" | "name" | "email">;
// = { id: number; name: string; email: string }

// Pour une carte d'utilisateur dans l'UI
type UserCardProps = Pick<User, "name" | "email" | "age">;
// = { name: string; email: string; age: number }

// Pour un endpoint de recherche
type UserSearchResult = Pick<User, "id" | "name">;
// = { id: number; name: string }

function renderUserCard(user: UserCardProps) {
  return `${user.name} (${user.age}) — ${user.email}`;
}

Omit<T, K> — Supprimer certaines clĂ©s

L'inverse de Pick : on enlÚve les clés qu'on ne veut pas.

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  password: string;
  createdAt: Date;
}

// Exclure les champs sensibles pour l'API publique
type SafeUser = Omit<User, "password">;
// = { id: number; name: string; email: string; age: number; createdAt: Date }

// Exclure les champs auto-générés pour la création
type CreateUserDTO = Omit<User, "id" | "createdAt">;
// = { name: string; email: string; age: number; password: string }

function createUser(data: CreateUserDTO): User {
  return {
    ...data,
    id: generateId(),        // Auto-généré
    createdAt: new Date(),   // Auto-généré
  };
}

Pick vs Omit — Quand utiliser lequel ?

Pick<T, K>

Quand tu veux peu de clés d'un type large.

// 3 clés sur 20

Pick<User, "id" | "name" | "email">

→ Plus explicite quand on garde peu

Omit<T, K>

Quand tu veux presque tout sauf quelques clés.

// Tout sauf 2 clés

Omit<User, "password" | "createdAt">

→ Plus explicite quand on enlùve peu

💡 Rùgle simple

Si tu gardes < 50% des clĂ©s → Pick. Si tu enlĂšves < 50% des clĂ©s → Omit.

Record<K, V> — Dictionnaires typĂ©s

CrĂ©er un objet dont toutes les clĂ©s ont le mĂȘme type de valeur.

// Record<string, number> = { [key: string]: number }
const scores: Record<string, number> = {
  alice: 95,
  bob: 87,
  charlie: 92,
};

// Record avec une union de clĂ©s — TOUTES les clĂ©s sont requises
type Status = "pending" | "active" | "archived";

const statusLabels: Record<Status, string> = {
  pending: "En attente",
  active: "Actif",
  archived: "Archivé",
};
// ❌ ERREUR si on oublie une clĂ© !

// Record avec des valeurs complexes
type Theme = "light" | "dark";
interface ThemeColors { bg: string; text: string; accent: string; }

const themes: Record<Theme, ThemeColors> = {
  light: { bg: "#fff", text: "#000", accent: "#0066ff" },
  dark:  { bg: "#1a1a1a", text: "#fff", accent: "#66b3ff" },
};

Record vs Index Signature vs Map

Record<string, T>

Quand les clés sont un type connu (union ou string). Vérifie la complétude si union.

{ [key: string]: T }

Index signature classique. Identique Ă  Record<string, T> en pratique.

Map<K, V>

Quand les clés ne sont pas des strings (objets, etc.) ou qu'on a besoin de .size, .has(), etc.

// ✅ PrĂ©fĂ©rer Record quand on connaĂźt les clĂ©s possibles
const permissions: Record<"admin" | "user" | "guest", boolean> = {
  admin: true,
  user: true,
  guest: false,
};

Module 5

Combiner les types utilitaires

Partial<Omit<User, 'id'>> — Le DTO de mise à jour

Le pattern DTO avec les utilitaires

Combiner Partial + Omit pour des DTOs précis et sûrs.

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  role: "admin" | "user";
  createdAt: Date;
}

// 1ïžâƒŁ DTO de crĂ©ation : tout sauf id et createdAt (auto-gĂ©nĂ©rĂ©s)
type CreateUserDTO = Omit<User, "id" | "createdAt">;
// = { name: string; email: string; age: number; role: "admin" | "user" }

// 2ïžâƒŁ DTO de mise Ă  jour : partiel, sans id (on ne change pas l'id)
type UpdateUserDTO = Partial<Omit<User, "id" | "createdAt">>;
// = { name?: string; email?: string; age?: number; role?: "admin" | "user" }

// 3ïžâƒŁ DTO de rĂ©ponse API : tout sauf le mot de passe
type UserResponse = Omit<User, "password">;

🔑 L'ordre compte

Partial<Omit<T, K>> : d'abord on enlÚve les clés, ensuite on rend optionnel. Lire de l'intérieur vers l'extérieur.

Exercice live : API CRUD complĂšte

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  category: string;
  stock: number;
  createdAt: Date;
  updatedAt: Date;
}

// Création : pas d'id ni de timestamps
type CreateProductDTO = Omit<Product, "id" | "createdAt" | "updatedAt">;

// Mise Ă  jour : partiel, sans id ni timestamps
type UpdateProductDTO = Partial<Omit<Product, "id" | "createdAt" | "updatedAt">>;

// Liste : juste les infos essentielles
type ProductListItem = Pick<Product, "id" | "name" | "price" | "category">;

// Filtre de recherche : toutes les clés optionnelles
type ProductFilter = Partial<Pick<Product, "category" | "name">>;

// Implémentation
async function updateProduct(id: number, data: UpdateProductDTO): Promise<Product> {
  // data peut contenir { name: "New name" } ou { price: 29.99 } ou les deux...
  return api.patch(`/products/${id}`, data);
}

Combinaisons avancées

// Required + Pick : rendre certaines clés obligatoires
type RequiredUserFields = Required<Pick<Partial<User>, "name" | "email">>;
// = { name: string; email: string }

// Record + Pick : dictionnaire de sous-types
type UsersByRole = Record<User["role"], User[]>;
// = { admin: User[]; user: User[] }

// Intersection : ajouter des champs Ă  un type existant
type UserWithToken = User & { token: string };

// Pattern complet d'un formulaire
interface FormState<T> {
  values: Partial<T>;
  errors: Partial<Record<keyof T, string>>;
  touched: Partial<Record<keyof T, boolean>>;
  isValid: boolean;
}

type UserForm = FormState<CreateUserDTO>;
// values: { name?: string; email?: string; ... }
// errors: { name?: string; email?: string; ... }
// touched: { name?: boolean; email?: boolean; ... }

PiÚge : extends dans les generics vs héritage

✗ Confusion courante

// Héritage de classe
class Animal { name: string; }
class Dog extends Animal { 
  breed: string; 
}
// Dog HÉRITE de Animal
// Dog est un sous-type

✓ Contrainte de generic

// Contrainte de type
function log<T extends { name: string }>(
  item: T
) {
  console.log(item.name);
}
// T doit AVOIR name
// T peut avoir d'autres props

⚠ MĂȘme mot-clĂ©, sens diffĂ©rent

Dans les classes : extends = hérite du comportement. Dans les generics : extends = "est assignable à" / "est compatible avec".

PiĂšges courants avec les types utilitaires

⚠

Submergé par trop d'utilitaires d'un coup

Commencer par Partial et Omit — les plus utiles au quotidien

✓ Apprendre 2 utilitaires à fond vaut mieux que 5 superficiellement

⚠

keyof semble abstrait sans exemples concrets

Toujours montrer le résultat du hover dans VS Code

✓ Écrire le code → hover → voir le type rĂ©solu

⚠

Oublier l'ordre de lecture (intĂ©rieur → extĂ©rieur)

Partial<Omit<User, 'id'>> se lit : d'abord Omit, puis Partial

✓ DĂ©composer en types intermĂ©diaires pour comprendre

Points clés à retenir

1. extends = contrainte

<T extends X> signifie "T doit avoir au moins les propriétés de X"

2. keyof = union des clés

keyof T donne toutes les clĂ©s — trĂšs puissant combinĂ© avec T[K]

3. Partial + Omit = DTOs

Partial<Omit<T, 'id'>> est LE pattern pour les mises Ă  jour

4. Record = dictionnaire

Record<K, V> type les objets avec clés connues et valeurs uniformes

5. Combiner les utilitaires

Les types utilitaires se composent : lire de l'intérieur vers l'extérieur, décomposer si nécessaire