TypeScript types avances - 01 - Mapped types : construire des types dynamiquement

Comment itérer sur les clés d'un type pour en construire un nouveau. Le mecanisme derrière Partial, Required, Readonly et les types custom.

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 T produit l'union des clés de T (ex: "name" | "email" | "age")
  • K in itéré sur chaque clé de cette union
  • T[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 -readonly retirent les optionnels et le readonly
  • Le remapping as permet 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

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.