Contraintes · Generics multiples · Types utilitaires
Partial · Pick · Omit · Record · keyof · extends
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
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
Recap : Generics de base
Ce qu'on a vu la derniĂšre fois
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.
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
Contraintes : <T extends ...>
Limiter ce que T peut ĂȘtre
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"
// 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'
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 }
| 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 |
keyof & Generics multiples
Extraire les clés d'un type et combiner <T, K>
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.
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é
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.
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'
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
Types utilitaires
Partial · Required · Pick · Omit · Record
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 |
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
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.
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}`;
}
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<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.
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<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,
};
Combiner les types utilitaires
Partial<Omit<User, 'id'>> â Le DTO de mise Ă jour
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.
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);
}
// 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; ... }
â 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".
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
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