frontend / typescript
Mapped types
Explication
Ce que vous allez apprendre
- Écrire un mapped type de base avec
{ [K in keyof T]: T[K] } - Ajouter ou retirer
readonly/?sur chaque propriété avec les modificateurs+/- - Renommer une clé pendant le parcours avec le key remapping (
as) - Filtrer certaines clés hors du résultat en les redirigeant vers
never - Combiner un mapped type avec un type conditionnel pour ne garder que certaines propriétés
Dans quel contexte ?
Une équipe a besoin de générer automatiquement, à partir d'une interface Produit { id, nom, prix }, un type ProduitGetters avec des méthodes getId(), getNom(), getPrix(), sans les écrire à la main ni les resynchroniser manuellement à chaque ajout de champ à Produit. En comprenant le mécanisme derrière Partial et Readonly (déjà utilisés en leçon 8), l'équipe peut écrire son propre mapped type avec key remapping : type Getters<T> = { [K in keyof T as \get${Capitalize<string & K>}`]: () => T[K] }, qui se met à jour automatiquement si Produit` change.
Transformer un type sans le réécrire
Un mapped type parcourt chaque propriété d'un type existant pour produire un nouveau type, en appliquant la même transformation à chacune. C'est le mécanisme qui se cache derrière des utilitaires que vous utilisez déjà, comme Partial ou Readonly : plutôt que de les considérer comme de la magie fournie par TypeScript, cette leçon montre comment les construire soi-même, et donc comment en créer de nouveaux adaptés à des besoins précis.
Prérequis
Cette leçon suppose les utility types (leçon 8) et les types conditionnels avec infer (leçon 13) déjà vus : les mapped types combinent souvent les deux pour des transformations précises.
La syntaxe de base
{ [K in keyof T]: T[K] } se lit littéralement : pour chaque clé K du type T, produire une propriété de même nom et du type correspondant. À partir de cette base, on peut ajouter des modificateurs +/- devant readonly ou ? pour ajouter ou retirer explicitement ces qualités à chaque propriété.
| Syntaxe | Effet |
|---|---|
{ [K in keyof T]: T[K] } | Copie chaque propriété telle quelle |
{ readonly [K in keyof T]: T[K] } | Ajoute readonly à chaque propriété |
{ -readonly [K in keyof T]: T[K] } | Retire readonly de chaque propriété |
{ [K in keyof T as NouveauNom]: T[K] } | Renomme chaque clé (key remapping) |
Renommer et filtrer avec as
Une extension plus avancée, le key remapping via le mot-clé as, permet de transformer le nom de chaque clé pendant le parcours — par exemple préfixer chaque propriété par get pour générer automatiquement des types de getters. Le même mécanisme permet aussi de filtrer certaines clés hors du résultat en les redirigeant vers never.
Astuce
Pour exclure une clé d'un mapped type selon une condition, redirigez-la vers never dans la clause as : [K in keyof T as T[K] extends Function ? K : never]. TypeScript élimine automatiquement toute propriété dont la clé calculée vaut never.
Combiner avec les conditionnels
Le vrai potentiel des mapped types apparaît en les combinant à un type conditionnel dans la clause as : on peut alors ne garder que les clés dont la valeur correspond à un certain critère, par exemple ne garder que les méthodes d'une classe. C'est un prolongement naturel de la leçon précédente sur les types conditionnels et infer.
Commandes & code
Mapped types
// Mapped type de base : applique une transformation à CHAQUE clé de T
type LectureSeule<T> = {
readonly [K in keyof T]: T[K];
};
interface Produit {
id: number;
nom: string;
prix: number;
}
type ProduitLectureSeule = LectureSeule<Produit>;
// Modificateurs +/- : ajouter ou retirer readonly / optionnel explicitement
type Optionnel<T> = {
[K in keyof T]+?: T[K]; // équivalent à Partial<T> "maison"
};
type Obligatoire<T> = {
[K in keyof T]-?: T[K]; // retire l'optionnalité, équivalent à Required<T>
};
type Modifiable<T> = {
-readonly [K in keyof T]: T[K]; // retire le readonly
};
// Key remapping avec "as" : renomme ou filtre les clés pendant le mapping
type PrefixerGetters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type GettersProduit = PrefixerGetters<Produit>;
// { getId: () => number; getNom: () => string; getPrix: () => number }
// Filtrer des clés avec "as never" (les exclut du résultat)
type SansId<T> = {
[K in keyof T as Exclude<K, "id">]: T[K];
};
type ProduitSansId = SansId<Produit>; // { nom: string; prix: number }
// Mapped type conditionnel : ne garde que les clés dont la valeur est une fonction
type SeulementMethodes<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
class Api {
url = "";
get() {
return fetch(this.url);
}
post(data: unknown) {
return fetch(this.url, { method: "POST" });
}
}
type MethodesApi = SeulementMethodes<Api>; // { get: ...; post: ... }
// Mapped type sur un Record généré dynamiquement (validation de formulaire typée)
type Erreurs<T> = {
[K in keyof T]?: string;
};
function valider<T extends object>(donnees: T): Erreurs<T> {
return {}; // implémentation omise
}Résumé
[K in keyof T]itère sur les clés ;as ...permet de les renommer ou filtrer.+?/-?et+readonly/-readonlyajoutent ou retirent explicitement un modificateur.- Combiné à un conditionnel (
extends ... ? K : never), un mapped type filtre par valeur.
Exercices pratiques
Mission : générer les getters de Produit automatiquement
Objectif : Construire un mapped type avec key remapping qui génère des méthodes getters à partir d'une interface, sans jamais les recopier à la main.
Contexte
Une équipe a besoin d'un type ProduitGetters avec des méthodes getId(), getNom(), getPrix(), générées automatiquement à partir de l'interface Produit { id, nom, prix }, sans les écrire à la main ni les resynchroniser manuellement à chaque ajout de champ. Ta mission : construire ce mapped type avec key remapping.