Types mappés#
Les types mappés constituent l’une des fonctionnalités les plus puissantes du système de types de TypeScript. Ils permettent de transformer un type existant en un nouveau type en itérant sur ses propriétés, de la même façon qu’une boucle for...of itère sur les éléments d’un tableau. Grâce à cette mécanique, il devient possible de construire des variantes d’un type de manière programmatique — rendre toutes les propriétés optionnelles, en lecture seule, ou encore les renommer — sans jamais dupliquer de code. Les types mappés sont le fondement de la quasi-totalité des types utilitaires intégrés à TypeScript (Partial<T>, Required<T>, Readonly<T>, etc.) et sont indispensables pour écrire des bibliothèques génériques expressives.
Syntaxe de base#
Un type mappé suit la syntaxe suivante :
type MonTypeMappé<T> = {
[K in keyof T]: T[K];
};
Cette syntaxe se lit comme une boucle : pour chaque clé K appartenant aux clés de T, le type mappé crée une propriété de clé K dont la valeur est du type T[K]. L’expression keyof T produit un type union de toutes les clés de T, et T[K] est l”accès indexé (indexed access type) qui récupère le type associé à la clé K dans T.
interface Utilisateur {
id: number;
nom: string;
email: string;
}
// Copie exacte du type — utile comme point de départ
type CopiUtilisateur = {
[K in keyof Utilisateur]: Utilisateur[K];
};
// Équivaut exactement à Utilisateur
Cette forme de base ne fait rien d’intéressant par elle-même, mais elle constitue le squelette sur lequel on vient greffer des transformations. En modifiant la partie droite (T[K]), on transforme les types des valeurs. En modifiant la partie gauche (les modificateurs de clé), on transforme la forme des propriétés.
// Rendre toutes les propriétés optionnelles
type MonPartial<T> = {
[K in keyof T]?: T[K];
};
// Rendre toutes les propriétés en lecture seule
type MonReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Transformer les valeurs en tableaux
type Tableaux<T> = {
[K in keyof T]: T[K][];
};
type TableauxUtilisateur = Tableaux<Utilisateur>;
// { id: number[]; nom: string[]; email: string[] }
Définition 29 (Type mappé)
Un type mappé est une construction TypeScript de la forme { [K in Union]: Type } qui génère un nouveau type objet en itérant sur chacun des membres d’un type union. Lorsqu’on utilise keyof T comme union, on itère sur toutes les propriétés de T. Le résultat est un type dont les clés sont exactement les membres de l’union et dont les valeurs sont déterminées par l’expression Type (qui peut référencer K et, dans un contexte générique, T).
Modificateurs#
Les modificateurs agissent sur les qualificateurs des propriétés générées : l”optionnalité (marqueur ?) et le caractère en lecture seule (marqueur readonly). TypeScript permet non seulement d’ajouter ces modificateurs, mais aussi de les retirer explicitement grâce au préfixe -.
Ajouter et retirer l’optionnalité#
// Ajouter ? (équivalent à Partial<T>)
type Optionnel<T> = {
[K in keyof T]?: T[K];
};
// Retirer ? (équivalent à Required<T>)
type Obligatoire<T> = {
[K in keyof T]-?: T[K];
};
interface Config {
hôte?: string;
port?: number;
ssl?: boolean;
}
type ConfigComplète = Obligatoire<Config>;
// { hôte: string; port: number; ssl: boolean }
// Toutes les propriétés sont maintenant obligatoires
Le préfixe -? est capital : sans lui, il n’existe aucune autre façon de supprimer le marqueur d’optionnalité d’une propriété dans un type mappé. Le préfixe +? est également valide mais rarement écrit car il est équivalent à ? seul.
Ajouter et retirer readonly#
// Ajouter readonly (équivalent à Readonly<T>)
type Gelé<T> = {
readonly [K in keyof T]: T[K];
};
// Retirer readonly (équivalent à -readonly, sans équivalent utilitaire intégré)
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
interface PointGelé {
readonly x: number;
readonly y: number;
}
type PointMutable = Mutable<PointGelé>;
// { x: number; y: number }
// Les propriétés peuvent maintenant être réassignées
Remarque 21
Les modificateurs +/- peuvent être combinés librement. Un type qui rend toutes les propriétés à la fois obligatoires et mutables s’écrit ainsi :
type Normalisé<T> = {
-readonly [K in keyof T]-?: T[K];
};
Cette combinaison est particulièrement utile pour créer des « snapshots » mutables d’objets normalement immuables, ou pour normaliser des interfaces dont certaines propriétés seraient accidentellement optionnelles ou en lecture seule.
## Remappage de clés avec `as`
Depuis TypeScript 4.1, la clause `as` permet de **remapper les clés** d'un type mappé. Au lieu de conserver la clé `K` telle quelle, on peut la transformer en un nouveau type de clé.
```typescript
type Remappé<T> = {
[K in keyof T as NouvelleCléDepuis<K>]: T[K];
};
Filtrer des clés avec never#
Lorsque la clause as produit le type never, TypeScript supprime la propriété correspondante du type résultant. C’est le mécanisme fondamental pour filtrer des propriétés selon un critère arbitraire.
// Conserver uniquement les propriétés dont la valeur est une chaîne
type SeulementChaînes<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Produit {
id: number;
nom: string;
description: string;
prix: number;
catégorie: string;
}
type PropriétésChaînes = SeulementChaînes<Produit>;
// { nom: string; description: string; catégorie: string }
// Supprimer les propriétés commençant par un underscore (convention privée)
type SansPrivé<T> = {
[K in keyof T as K extends `_${string}` ? never : K]: T[K];
};
interface InterneProduit {
id: number;
nom: string;
_cache: unknown;
_version: number;
}
type ProduitPublic = SansPrivé<InterneProduit>;
// { id: number; nom: string }
Renommer avec les template literal types#
La clause as se combine naturellement avec les template literal types pour renommer des clés selon un patron :
// Préfixer chaque clé par "get"
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Cercle {
rayon: number;
couleur: string;
}
type GettersCercle = Getters<Cercle>;
// { getRayon: () => number; getCouleur: () => string }
Exemple 9 (Remappage bidirectionnel)
On peut créer simultanément des getters et des setters à partir d’un même type source en utilisant un type mappé combiné à une intersection :
type Accesseurs<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
} & {
[K in keyof T as `set${Capitalize<string & K>}`]: (valeur: T[K]) => void;
};
type AccesseursCercle = Accesseurs<Cercle>;
// {
// getRayon: () => number;
// getCouleur: () => string;
// setRayon: (valeur: number) => void;
// setCouleur: (valeur: string) => void;
// }
## Types mappés et conditionnels combinés
La véritable puissance des types mappés émerge lorsqu'on les combine avec les types conditionnels. Cette combinaison permet des transformations récursives et des filtrages arbitrairement complexes.
### `DeepReadonly<T>` complet
Le type utilitaire intégré `Readonly<T>` n'opère qu'en surface : si une propriété est elle-même un objet, ses sous-propriétés restent mutables. `DeepReadonly<T>` résout ce problème de façon récursive :
```typescript
type DeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Arbre {
valeur: number;
enfants: {
gauche: { valeur: number };
droit: { valeur: number };
};
}
type ArbreGelé = DeepReadonly<Arbre>;
// {
// readonly valeur: number;
// readonly enfants: {
// readonly gauche: { readonly valeur: number };
// readonly droit: { readonly valeur: number };
// };
// }
TypeScript gère automatiquement les récursions de profondeur raisonnable. Pour des structures très profondes ou circulaires, il peut être nécessaire d’ajouter une limite de profondeur.
Filtrer les propriétés par type de valeur#
// Extraire uniquement les propriétés dont la valeur est de type Filtre
type ExtraireParType<T, Filtre> = {
[K in keyof T as T[K] extends Filtre ? K : never]: T[K];
};
// Supprimer les propriétés dont la valeur est de type Filtre
type OmettreParType<T, Filtre> = {
[K in keyof T as T[K] extends Filtre ? never : K]: T[K];
};
interface FormulairePaiement {
montant: number;
devise: string;
cardToken: string;
estSécurisé: boolean;
métadonnées: Record<string, unknown>;
rappel: () => void;
}
type ChampsTextuels = ExtraireParType<FormulairePaiement, string>;
// { devise: string; cardToken: string }
type ChampsSansFonctions = OmettreParType<FormulairePaiement, Function>;
// { montant: number; devise: string; cardToken: string;
// estSécurisé: boolean; métadonnées: Record<string, unknown> }
Template literal types#
Les template literal types étendent la syntaxe des littéraux de gabarit JavaScript (\…``) au niveau des types. Ils permettent de construire des types de chaînes de caractères en combinant des types existants.
Définition 30 (Template literal type)
Un template literal type est un type de chaîne construit par interpolation d’autres types dans un gabarit. La syntaxe est identique à celle des littéraux de gabarit JavaScript : `préfixe${TypeInterpolé}suffixe`. Lorsque TypeInterpolé est un type union, TypeScript génère automatiquement toutes les combinaisons possibles (distribution du type union).
type Direction = "haut" | "bas" | "gauche" | "droite";
// Distribution automatique sur l'union
type CssFlex = `flex-${Direction}`;
// "flex-haut" | "flex-bas" | "flex-gauche" | "flex-droite"
// Combinaison de plusieurs unions — produit cartésien
type Taille = "sm" | "md" | "lg";
type Couleur = "rouge" | "vert" | "bleu";
type Classe = `btn-${Taille}-${Couleur}`;
// "btn-sm-rouge" | "btn-sm-vert" | "btn-sm-bleu"
// | "btn-md-rouge" | ... (9 combinaisons au total)
Types de manipulation de chaînes intégrés#
TypeScript fournit quatre types utilitaires de manipulation de chaînes, applicables aux types littéraux :
type E1 = Uppercase<"bonjour">; // "BONJOUR"
type E2 = Lowercase<"BONJOUR">; // "bonjour"
type E3 = Capitalize<"bonjour">; // "Bonjour"
type E4 = Uncapitalize<"Bonjour">; // "bonjour"
Construire des types d’événements#
Un cas d’usage classique est la construction de noms d’écouteurs d’événements à partir d’une liste d’événements :
type Événements = "click" | "focus" | "blur" | "change" | "submit";
// Génère : "onClick" | "onFocus" | "onBlur" | "onChange" | "onSubmit"
type GestionnaireÉvénement = `on${Capitalize<Événements>}`;
// Créer un type objet pour les gestionnaires d'événements HTML
type GestionnairesHTML<É extends string> = {
[K in É as `on${Capitalize<K>}`]?: (événement: Event) => void;
};
type GestionnairesFormulaire = GestionnairesHTML<"submit" | "reset" | "change">;
// {
// onSubmit?: (événement: Event) => void;
// onReset?: (événement: Event) => void;
// onChange?: (événement: Event) => void;
// }
Remarque 22
Les template literal types et les types mappés sont particulièrement bien adaptés à la définition de bibliothèques dont l’API doit être cohérente et prédictible. Par exemple, une bibliothèque de gestion d’état peut dériver automatiquement les noms des mutations (SET_NOM, SET_EMAIL) à partir des noms des propriétés du modèle, garantissant que l’ensemble de l’API est toujours synchronisé avec la définition du type de données.
Exemples pratiques#
Getters<T>#
Un type qui transforme un objet en une interface de getters, fréquemment utilisé dans les architectures orientées objet pour encapsuler l’accès aux propriétés :
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface CompteBancaire {
solde: number;
propriétaire: string;
iban: string;
actif: boolean;
}
type GettersCompte = Getters<CompteBancaire>;
// {
// getSolde: () => number;
// getPropriétaire: () => string;
// getIban: () => string;
// getActif: () => boolean;
// }
// Implémentation possible avec un type intersection
function créerGetters<T extends object>(obj: T): T & Getters<T> {
const getters: Record<string, () => unknown> = {};
for (const clé of Object.keys(obj) as (keyof T)[]) {
const nomGetter = `get${String(clé).charAt(0).toUpperCase()}${String(clé).slice(1)}`;
getters[nomGetter] = () => obj[clé];
}
return Object.assign(obj, getters) as T & Getters<T>;
}
EventHandlers<T>#
Ce type crée une carte de gestionnaires d’événements dont les signatures sont strictement typées d’après la définition des événements :
// Carte d'événements : nom de l'événement → type du payload
interface ÉvénementsBoutique {
ajouterAuPanier: { produitId: string; quantité: number };
supprimerDuPanier: { produitId: string };
viderPanier: void;
appliquerCoupon: { code: string; réduction: number };
}
// Générer les gestionnaires d'événements correspondants
type GestionnairesBoutique = {
[K in keyof ÉvénementsBoutique]: (
payload: ÉvénementsBoutique[K]
) => void;
};
// Un bus d'événements fortement typé
type BusÉvénements<É> = {
on<K extends keyof É>(
événement: K,
gestionnaire: (payload: É[K]) => void
): void;
émettre<K extends keyof É>(événement: K, payload: É[K]): void;
};
FlattenObject<T>#
Un type qui aplatit les objets imbriqués en utilisant une notation pointée pour les clés :
type Chemin<T, Préfixe extends string = ""> = {
[K in keyof T & string]: T[K] extends object
? Chemin<T[K], `${Préfixe}${K}.`>
: { [P in `${Préfixe}${K}`]: T[K] };
}[keyof T & string];
type FlattenObject<T> = UnionToIntersection<Chemin<T>>;
// Type auxiliaire
type UnionToIntersection<U> =
(U extends unknown ? (x: U) => void : never) extends (x: infer I) => void
? I
: never;
interface AdresseImbriquée {
rue: string;
ville: {
nom: string;
codePostal: string;
};
coordonnées: {
latitude: number;
longitude: number;
};
}
type AdressePlate = FlattenObject<AdresseImbriquée>;
// {
// "rue": string;
// "ville.nom": string;
// "ville.codePostal": string;
// "coordonnées.latitude": number;
// "coordonnées.longitude": number;
// }
Exemple 10 (Validation de formulaire avec types mappés)
Les types mappés permettent de dériver automatiquement le type des erreurs de validation à partir du type du formulaire :
interface FormulaireInscription {
nom: string;
email: string;
motDePasse: string;
âge: number;
}
// Chaque champ peut avoir un message d'erreur (ou undefined si valide)
type ErreursFormulaire<T> = {
[K in keyof T]?: string;
};
// État complet du formulaire
type ÉtatFormulaire<T> = {
valeurs: T;
erreurs: ErreursFormulaire<T>;
estSoumis: boolean;
champsTouchés: { [K in keyof T]?: boolean };
};
type ÉtatInscription = ÉtatFormulaire<FormulaireInscription>;
```{code-cell} python
:tags: [hide-input]
fig, ax = plt.subplots(figsize=(14, 7))
ax.set_xlim(0, 14)
ax.set_ylim(0, 8)
ax.axis('off')
ax.set_title('Transformation par un type mappé', fontsize=16,
fontweight='bold', pad=20)
palette = sns.color_palette("muted", 6)
bleu = palette[0]
orange = palette[1]
vert = palette[2]
rouge = palette[3]
violet = palette[4]
gris_fond = '#f5f5f5'
# ---- Type source ----
src_x, src_y = 0.4, 1.2
src_w, src_h = 4.0, 5.6
src_box = patches.FancyBboxPatch(
(src_x, src_y), src_w, src_h,
boxstyle="round,pad=0.15", linewidth=2.5,
edgecolor=bleu, facecolor=bleu, alpha=0.12)
ax.add_patch(src_box)
src_border = patches.FancyBboxPatch(
(src_x, src_y), src_w, src_h,
boxstyle="round,pad=0.15", linewidth=2.5,
edgecolor=bleu, facecolor='none')
ax.add_patch(src_border)
ax.text(src_x + src_w / 2, src_y + src_h - 0.45,
'Type source T', ha='center', va='center',
fontsize=13, fontweight='bold', color=bleu)
propriétés_src = [
('id', 'number', bleu),
('nom', 'string', vert),
('email', 'string', vert),
('actif', 'boolean', orange),
('créé', 'Date', rouge),
]
for i, (clé, valeur, couleur) in enumerate(propriétés_src):
py = src_y + src_h - 1.2 - i * 0.85
prop_box = patches.FancyBboxPatch(
(src_x + 0.25, py - 0.28), src_w - 0.5, 0.56,
boxstyle="round,pad=0.08", linewidth=1.2,
edgecolor=couleur, facecolor=couleur, alpha=0.18)
ax.add_patch(prop_box)
ax.text(src_x + 0.65, py,
clé, ha='left', va='center',
fontsize=10, fontweight='bold', color='#333333',
fontfamily='monospace')
ax.text(src_x + src_w - 0.35, py,
valeur, ha='right', va='center',
fontsize=10, color=couleur,
fontfamily='monospace')
# ---- Bloc de transformation ----
tr_x, tr_y = 5.2, 3.1
tr_w, tr_h = 3.6, 1.8
tr_box = patches.FancyBboxPatch(
(tr_x, tr_y), tr_w, tr_h,
boxstyle="round,pad=0.2", linewidth=2,
edgecolor=violet, facecolor=violet, alpha=0.12)
ax.add_patch(tr_box)
tr_border = patches.FancyBboxPatch(
(tr_x, tr_y), tr_w, tr_h,
boxstyle="round,pad=0.2", linewidth=2,
edgecolor=violet, facecolor='none')
ax.add_patch(tr_border)
ax.text(tr_x + tr_w / 2, tr_y + tr_h - 0.38,
'Type mappé', ha='center', va='center',
fontsize=11, fontweight='bold', color=violet)
ax.text(tr_x + tr_w / 2, tr_y + 0.72,
'[K in keyof T]?:', ha='center', va='center',
fontsize=9, color='#555555', fontfamily='monospace')
ax.text(tr_x + tr_w / 2, tr_y + 0.30,
'T[K] | undefined', ha='center', va='center',
fontsize=9, color='#555555', fontfamily='monospace')
# ---- Flèches source → transformation → résultat ----
ax.annotate('', xy=(tr_x, tr_y + tr_h / 2),
xytext=(src_x + src_w, tr_y + tr_h / 2),
arrowprops=dict(arrowstyle='->', color='#444444', lw=2.2))
ax.annotate('', xy=(9.6, tr_y + tr_h / 2),
xytext=(tr_x + tr_w, tr_y + tr_h / 2),
arrowprops=dict(arrowstyle='->', color='#444444', lw=2.2))
# ---- Type résultat ----
res_x, res_y = 9.6, 1.2
res_w, res_h = 4.0, 5.6
res_box = patches.FancyBboxPatch(
(res_x, res_y), res_w, res_h,
boxstyle="round,pad=0.15", linewidth=2.5,
edgecolor=vert, facecolor=vert, alpha=0.12)
ax.add_patch(res_box)
res_border = patches.FancyBboxPatch(
(res_x, res_y), res_w, res_h,
boxstyle="round,pad=0.15", linewidth=2.5,
edgecolor=vert, facecolor='none')
ax.add_patch(res_border)
ax.text(res_x + res_w / 2, res_y + res_h - 0.45,
'Partial<T>', ha='center', va='center',
fontsize=13, fontweight='bold', color=vert)
propriétés_res = [
('id?', 'number | undefined', bleu),
('nom?', 'string | undefined', vert),
('email?', 'string | undefined', vert),
('actif?', 'boolean | undefined', orange),
('créé?', 'Date | undefined', rouge),
]
for i, (clé, valeur, couleur) in enumerate(propriétés_res):
py = res_y + res_h - 1.2 - i * 0.85
prop_box = patches.FancyBboxPatch(
(res_x + 0.25, py - 0.28), res_w - 0.5, 0.56,
boxstyle="round,pad=0.08", linewidth=1.2,
edgecolor=couleur, facecolor=couleur, alpha=0.18)
ax.add_patch(prop_box)
ax.text(res_x + 0.55, py,
clé, ha='left', va='center',
fontsize=9, fontweight='bold', color='#333333',
fontfamily='monospace')
ax.text(res_x + res_w - 0.2, py,
valeur, ha='right', va='center',
fontsize=8, color=couleur,
fontfamily='monospace')
ax.text(7.0, 0.45,
'{ [K in keyof T]?: T[K] } → toutes les propriétés deviennent optionnelles',
ha='center', va='center', fontsize=9.5,
color='#555555', style='italic')
plt.tight_layout()
plt.show()
Résumé#
Dans ce chapitre, nous avons exploré les types mappés, une fonctionnalité fondamentale du système de types avancé de TypeScript :
La syntaxe de base
{ [K in keyof T]: T[K] }itère sur toutes les propriétés d’un type pour en produire un dérivé, à la manière d’une boucle sur les clés d’un objet.Les modificateurs
?/-?etreadonly/-readonlypermettent d’ajouter ou de retirer l’optionnalité et le caractère immuable des propriétés générées, sans dupliquer de code.La clause
as(TypeScript 4.1+) permet de remapper les clés : on peut les renommer avec des template literal types ou les filtrer en produisantneverpour certaines propriétés.La combinaison avec les types conditionnels ouvre la porte à des transformations récursives comme
DeepReadonly<T>et à des filtrages par type de valeur.Les template literal types permettent de construire des types de chaînes par interpolation, avec distribution automatique sur les unions, et servent de base à des conventions de nommage typées (
on${Capitalize<Event>}).Des types pratiques comme
Getters<T>,EventHandlers<T>ouFlattenObject<T>illustrent comment les types mappés simplifient la définition d’architectures entières.
Le chapitre suivant aborde une autre fonctionnalité avancée de TypeScript : les décorateurs, qui permettent d’annoter et de modifier des classes, méthodes et propriétés à la manière d’une métaprogrammation déclarative.