01 - Mapped types : construire des types dynamiquement
Ce que tu vas apprendre
- La syntaxe
[K in keyof T]et comment elle itéré sur les clés - Comment modifier les propriétés (optionnelles, readonly, renommees)
- Les modificateurs
+,-pour ajouter ou retirer readonly et? - Comment reconstruire Partial, Required, Readonly et en créer des custom
Prerequisites
Avoir lu les articles sur keyof et typeof et les utility types.
La boucle for des types
En JavaScript, tu peux itérer sur les clés d'un objet avec for...in :
typescriptfor (const key in user) {
console.log(key, user[key])
}
Les mapped types font la meme chose, mais au niveau des types. Tu iteres sur les clés d'un type et tu construis un nouveau type avec chaque clé transformee.
typescripttype MakeOptional<T> = {
[K in keyof T]?: T[K]
}
Decomposons :
keyof Tproduit l'union des clés de T (ex:"name" | "email" | "age")K initéré sur chaque clé de cette unionT[K]est le type de la propriété correspondante (index access type)?rend chaque propriété optionnelle
Le résultat : un nouveau type ou chaque propriété de T est optionnelle. C'est exactement Partial<T>.
Reconstruire les utility types
Une fois que tu comprends la syntaxe, tous les utility types de base deviennent limpides :
typescript// Partial : tout optionnel
type Partial<T> = {
[K in keyof T]?: T[K]
}
// Required : tout obligatoire
type Required<T> = {
[K in keyof T]-?: T[K]
}
// Readonly : tout en lecture seule
type Readonly<T> = {
readonly [K in keyof T]: T[K]
}
// Mutable : retirer readonly (l'inverse de Readonly)
type Mutable<T> = {
-readonly [K in keyof T]: T[K]
}
Le - devant ? ou readonly retire le modificateur. Le + l'ajoute (mais il est implicite, donc rarement écrit).
Filtrer les propriétés par type
Un mapped type peut filtrer les clés en fonction du type de la valeur. Par exemple, garder uniquement les propriétés de type string :
typescripttype StringKeysOf<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}
interface User {
id: number
name: string
email: string
age: number
}
type StringFields = StringKeysOf<User>
// { name: string; email: string }
Le as ... ? K : never est un filtre. Si la condition est vraie, on garde la clé. Si elle est fausse, never l'elimine. C'est le mecanisme de remapping introduit dans TypeScript 4.1.
Renommer les clés avec as
Le as dans un mapped type permet aussi de transformer les noms des clés :
typescripttype Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
interface User {
name: string
age: number
}
type UserGetters = Getters<User>
// {
// getName: () => string
// getAge: () => number
// }
Capitalize est un type utilitaire intégré qui met la première lettre en majuscule. string & K est nécessaire parce que keyof T peut contenir des number ou symbol, et Capitalize ne fonctionne que sur les strings.
Le meme principe pour des setters :
typescripttype Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void
}
type UserSetters = Setters<User>
// {
// setName: (value: string) => void
// setAge: (value: number) => void
// }
Patterns concrets
Rendre certaines propriétés optionnelles (pas toutes)
typescripttype PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
interface User {
id: string
name: string
email: string
age: number
}
type CreateUser = PartialBy<User, "age">
// { id: string; name: string; email: string; age?: number }
On combine Omit (enlever les clés qu'on veut rendre optionnelles), Pick + Partial (les reprendre en optionnel), et & (fusionner). C'est un pattern que j'utilise souvent pour les formulaires sur paltemps.fr : certains champs sont obligatoires, d'autres non.
Transformer les valeurs
typescripttype Nullable<T> = {
[K in keyof T]: T[K] | null
}
type NullableUser = Nullable<User>
// { id: string | null; name: string | null; email: string | null; age: number | null }
Utile quand tu recois des donnees d'une base ou chaque champ peut etre NULL.
Event map
typescripttype EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (data: T[K]) => void
}
interface Events {
click: { x: number; y: number }
submit: { formData: FormData }
error: { message: string; code: number }
}
type Handlers = EventHandlers<Events>
// {
// onClick: (data: { x: number; y: number }) => void
// onSubmit: (data: { formData: FormData }) => void
// onError: (data: { message: string; code: number }) => void
// }
Readonly sauf certaines propriétés
typescripttype ReadonlyExcept<T, K extends keyof T> = Readonly<Omit<T, K>> & Pick<T, K>
type UserWithEditableName = ReadonlyExcept<User, "name">
// name est mutable, tout le reste est readonly
Mapped types et generics
Les mapped types se combinent naturellement avec les generics pour créer des utilitaires reutilisables :
typescripttype DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}
Ce type recursif rend tout optionnel en profondeur. On le détaillé dans l'article sur les types recursifs.
Limites des mapped types
Les mapped types ne peuvent itérer que sur des clés (string | number | symbol). Tu ne peux pas itérer sur les valeurs directement.
typescript// ❌ Impossible
type Values<T> = {
[V in T[keyof T]]: ... // Erreur — V n'est pas une cle
}
Pour manipuler les valeurs, il faut passer par des conditional types (article suivant).
Les mapped types preservent les modificateurs (readonly, ?) de l'original par défaut. Si tu veux les changer, utilise explicitement +readonly, -readonly, +?, -?.
Résumé
- Un mapped type itéré sur les clés d'un type avec
[K in keyof T]pour construire un nouveau type - Les modificateurs
-?et-readonlyretirent les optionnels et le readonly - Le remapping
aspermet de renommer ou filtrer les clés - Partial, Required, Readonly sont des mapped types — tu peux en construire des custom
- Combine mapped types + generics + conditions pour créer des utilitaires reutilisables
Article précédent : 00 - Introduction
Article suivant : 02 - Conditional types et infer