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
anycomme 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