TypeScript types avances - 07 - Types recursifs : DeepPartial, chemins imbriques et au-delà

Comment écrire des types qui se referent a eux-memes. DeepPartial, DeepReadonly, chemins d'objets imbriques, et les limites de la recursion.

07 - Types recursifs : DeepPartial, chemins imbriques et au-delà

Ce que tu vas apprendre

  • Comment un type peut se référencer lui-meme
  • DeepPartial, DeepReadonly, DeepRequired : les versions profondes des utility types
  • Extraire les chemins d'un objet imbrique (dot notation)
  • Les limites de profondeur et comment les gerer

Prerequisites

Avoir lu les articles sur les mapped types et les conditional types.


Pourquoi les utility types s'arretent a un niveau

On l'a vu dans la serie 1 : Partial<T> ne rend optionnel que le premier niveau. Readonly<T> pareil. Si tu as un objet imbrique, les niveaux inférieurs ne sont pas touches.

typescriptinterface Config {
  server: {
    port: number
    cors: {
      origins: string[]
      credentials: boolean
    }
  }
  database: {
    host: string
    pool: {
      min: number
      max: number
    }
  }
}

type PartialConfig = Partial<Config>
// server?: { port: number; cors: { origins: string[]; credentials: boolean } }
// ❌ Les champs internes de server ne sont PAS optionnels

Pour descendre en profondeur, il faut un type recursif.

DeepPartial

typescripttype DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T

type PartialConfig = DeepPartial<Config>
// server?: {
//   port?: number
//   cors?: {
//     origins?: string[]
//     credentials?: boolean
//   }
// }
// database?: { host?: string; pool?: { min?: number; max?: number } }

Le type vérifié si T[K] est un object. Si oui, il rappelle DeepPartial recursivement. Sinon (primitif), il garde le type tel quel.

Attention : Array est aussi un object. Si tu veux que les tableaux restent intacts :

typescripttype DeepPartial<T> = T extends (infer U)[]
  ? DeepPartial<U>[]
  : T extends object
    ? { [K in keyof T]?: DeepPartial<T[K]> }
    : T

Ce pattern est celui que j'utilise sur paltemps.fr pour les formulaires de mise à jour partielle des configurations imbriquees.

DeepReadonly

typescripttype DeepReadonly<T> = T extends (infer U)[]
  ? readonly DeepReadonly<U>[]
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T

Meme logique. Chaque niveau recoit readonly. Les tableaux deviennent readonly.

DeepRequired

typescripttype DeepRequired<T> = T extends object
  ? { [K in keyof T]-?: DeepRequired<T[K]> }
  : T

Le -? retire l'optionnel a chaque niveau.

Chemins d'objets imbriques (dot notation)

Un pattern avance : extraire tous les chemins valides d'un objet sous forme de strings avec la dot notation.

typescripttype Paths<T> = T extends object
  ? {
      [K in keyof T & string]: T[K] extends object
        ? K | `${K}.${Paths<T[K]>}`
        : K
    }[keyof T & string]
  : never

type ConfigPaths = Paths<Config>
// "server" | "server.port" | "server.cors" | "server.cors.origins"
// | "server.cors.credentials" | "database" | "database.host"
// | "database.pool" | "database.pool.min" | "database.pool.max"

Le type généré recursivement chaque chemin en concatenant les clés avec des points.

Obtenir le type a un chemin

typescripttype GetValueAtPath<T, P extends string> =
  P extends `${infer Head}.${infer Tail}`
    ? Head extends keyof T
      ? GetValueAtPath<T[Head], Tail>
      : never
    : P extends keyof T
      ? T[P]
      : never

type PortType = GetValueAtPath<Config, "server.port">     // number
type OriginsType = GetValueAtPath<Config, "server.cors.origins"> // string[]
type BadPath = GetValueAtPath<Config, "server.foo">        // never

Ce pattern est utilise par des libs comme lodash.get type-safe ou les form libraries qui accedent aux champs par chemin.

Fonction get type-safe

typescriptfunction get<T, P extends Paths<T> & string>(
  obj: T,
  path: P
): GetValueAtPath<T, P> {
  return path.split(".").reduce((acc: any, key) => acc[key], obj)
}

const config: Config = { /* ... */ }
const port = get(config, "server.port")        // type: number ✅
const origins = get(config, "server.cors.origins") // type: string[] ✅
get(config, "server.foo")                       // ❌ erreur de compilation

L'autocompletion propose tous les chemins valides. Le type de retour est automatiquement infere.

Types recursifs pour les structures de donnees

Arbre

typescripttype TreeNode<T> = {
  value: T
  children: TreeNode<T>[]
}

const tree: TreeNode<string> = {
  value: "root",
  children: [
    { value: "a", children: [] },
    {
      value: "b",
      children: [
        { value: "b1", children: [] }
      ]
    }
  ]
}

JSON type

typescripttype JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue }

Ce type represente toute valeur JSON valide. Il se référencé dans les tableaux et les objets.

Liste chaînée

typescripttype LinkedList<T> =
  | { value: T; next: LinkedList<T> }
  | null

const list: LinkedList<number> = {
  value: 1,
  next: {
    value: 2,
    next: {
      value: 3,
      next: null
    }
  }
}

Limites de la recursion

TypeScript a une limite de profondeur pour les types recursifs. Autour de 50 niveaux pour les types instanties et environ 1000 pour les conditional types (depuis TypeScript 4.5 qui a augmente la limite).

typescript// ❌ Trop profond
type InfiniteNest<T> = {
  value: T
  nested: InfiniteNest<InfiniteNest<T>> // double recursion = explosion
}

Si tu atteins la limite, le compilateur affiche "Type instantiation is excessively deep and possibly infinite".

Stratégies pour éviter :

  • Ajoute un compteur de profondeur avec un tuple qui grandit
  • Limite la recursion a un nombre raisonnable de niveaux (5-10 suffit en pratique)
  • Utilise any comme cas de base pour couper la recursion
typescript// Limiter la profondeur avec un compteur
type DeepPartial<T, Depth extends unknown[] = []> =
  Depth["length"] extends 5
    ? T // arrete apres 5 niveaux
    : T extends object
      ? { [K in keyof T]?: DeepPartial<T[K], [...Depth, unknown]> }
      : T

Résumé

  • Un type recursif se référencé lui-meme pour descendre dans les structures imbriquees
  • DeepPartial, DeepReadonly, DeepRequired appliquent des transformations a tous les niveaux d'un objet
  • Les chemins dot-notation sont extraits avec des template literal types recursifs
  • TypeScript a une limite de profondeur (~50 niveaux) — ajoute un compteur ou un cas de base pour les structures tres profondes
  • Les types recursifs sont le fondement de JsonValue, TreeNode, et des types de libs comme Prisma

Article précédent : 06 - satisfies, as const et const generics

Article suivant : 08 - Variance : covariance et contravariance

Sources

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