Retour au cours

frontend / typescript

Mapped types

Leçon 141 exercice

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é.

SyntaxeEffet
{ [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

ts
// 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/-readonly ajoutent ou retirent explicitement un modificateur.
  • Combiné à un conditionnel (extends ... ? K : never), un mapped type filtre par valeur.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →