03 - Distributive conditional types : le piège
Ce que tu vas apprendre
- Ce qu'est la distribution des conditional types sur les unions
- Pourquoi ca produit des résultats inattendus
- Le pattern
[T] extends [U]pour désactiver la distribution - Quand la distribution est utile et quand elle est un piège
Prerequisites
Avoir lu l'article sur les conditional types et infer.
Le résultat qui ne devrait pas exister
Regarde ce type :
typescripttype IsString<T> = T extends string ? "yes" : "no"
type A = IsString<string> // "yes" ✅
type B = IsString<number> // "no" ✅
type C = IsString<string | number> // ???
Intuitivement, string | number n'est pas assignable a string, donc le résultat devrait etre "no". Mais TypeScript retourne "yes" | "no".
C'est la distribution. Quand un conditional type recoit une union, TypeScript applique la condition a chaque membre de l'union individuellement, puis recombine les résultats en une union.
typescript// TypeScript decompose :
IsString<string | number>
// = IsString<string> | IsString<number>
// = "yes" | "no"
Quand la distribution est utile
La distribution est la raison pour laquelle Exclude et Extract fonctionnent :
typescripttype Exclude<T, U> = T extends U ? never : T
type Result = Exclude<"a" | "b" | "c", "a">
// = ("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b") | ("c" extends "a" ? never : "c")
// = never | "b" | "c"
// = "b" | "c"
Sans distribution, "a" | "b" | "c" serait évalué comme un tout, et le résultat serait différent. La distribution permet de filtrer membre par membre.
Meme chose pour NonNullable :
typescripttype NonNullable<T> = T extends null | undefined ? never : T
type Result = NonNullable<string | null | undefined>
// = string | never | never
// = string
Quand la distribution est un piège
Le problème arrive quand tu ne t'y attends pas. Un exemple concret que j'ai eu sur un projet :
typescripttype ToArray<T> = T extends any ? T[] : never
type A = ToArray<string> // string[]
type B = ToArray<number> // number[]
type C = ToArray<string | number> // string[] | number[] ❌
Je voulais (string | number)[]. J'ai eu string[] | number[]. Ce n'est pas la meme chose :
typescript// string[] | number[] = un tableau de strings OU un tableau de numbers
const a: string[] | number[] = [1, 2, 3] // ✅
const b: string[] | number[] = ["a", 1] // ❌ melange interdit
// (string | number)[] = un tableau qui peut contenir les deux
const c: (string | number)[] = ["a", 1] // ✅
Désactiver la distribution
Pour empecher la distribution, enveloppe T et la condition dans des tuples :
typescript// ❌ Distributif
type ToArray<T> = T extends any ? T[] : never
// ✅ Non-distributif
type ToArray<T> = [T] extends [any] ? T[] : never
type C = ToArray<string | number> // (string | number)[] ✅
Le pattern [T] extends [U] empeche TypeScript de décomposer l'union. Au lieu de tester chaque membre individuellement, il teste l'union entière comme un seul type.
IsNever avec distribution
Un cas classique : détecter never.
typescript// ❌ Ne fonctionne pas
type IsNever<T> = T extends never ? true : false
type A = IsNever<never> // never ❌ (devrait etre true)
Pourquoi never ? Parce que never est une union vide. La distribution itéré sur zero membres et produit une union vide, donc never.
typescript// ✅ Fonctionne
type IsNever<T> = [T] extends [never] ? true : false
type A = IsNever<never> // true ✅
type B = IsNever<string> // false ✅
IsUnion
Détecter si un type est une union :
typescripttype IsUnion<T, Copy = T> = [T] extends [never]
? false
: T extends T
? [Copy] extends [T]
? false
: true
: never
type A = IsUnion<string> // false
type B = IsUnion<string | number> // true
C'est un trick avance. T extends T déclenché la distribution (chaque membre est teste). [Copy] extends [T] vérifié si l'union complète est assignable a chaque membre. Si oui, ce n'est pas une union.
Regles de la distribution
La distribution se produit quand :
- Le type est un conditional type (
T extends U ? X : Y) - Le type teste (
T) est un paramètre de type nu (naked type parameter) - Le paramètre de type recoit une union
"Nu" signifie que T est utilise directement, pas enveloppe dans un autre type.
typescript// ✅ Distributif — T est nu
type D<T> = T extends string ? "yes" : "no"
// ❌ Non-distributif — T est enveloppe
type D<T> = [T] extends [string] ? "yes" : "no"
type D<T> = { value: T } extends { value: string } ? "yes" : "no"
type D<T> = T[] extends string[] ? "yes" : "no"
Exemple pratique : filtrer les propriétés
La distribution est utile pour créer des filtres de propriétés :
typescripttype StringKeys<T> = {
[K in keyof T]: T[K] extends string ? K : never
}[keyof T]
interface User {
id: number
name: string
email: string
age: number
}
type SK = StringKeys<User> // "name" | "email"
Le mapped type associe chaque clé a soit elle-meme (si la valeur est un string) soit never. L'index access [keyof T] collecte les résultats en une union, et never disparaît des unions.
Sur paltemps.fr, j'utilise ce pattern pour générer automatiquement les champs de recherche texte à partir du type d'une entité : seules les propriétés string sont indexees dans la recherche.
Résumé
- Les conditional types se distribuent automatiquement sur les unions quand le paramètre de type est "nu"
- La distribution décomposé
T<A | B>enT<A> | T<B>— utile pourExclude,Extract,NonNullable - Le pattern
[T] extends [U]désactivé la distribution en enveloppant le type neverest une union vide : les conditional types distributifs surneverretournentnever- Verifie toujours le comportement de tes conditional types avec des unions avant de les utiliser
Article précédent : 02 - Conditional types et infer
Article suivant : 04 - Template literal types