TypeScript types avances - 03 - Distributive conditional types : le piège

Pourquoi les conditional types se distribuent sur les unions, comment ca piège, et comment contrôler ce comportement avec le pattern [T] extends [U].

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 :

  1. Le type est un conditional type (T extends U ? X : Y)
  2. Le type teste (T) est un paramètre de type nu (naked type parameter)
  3. 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> en T<A> | T<B> — utile pour Exclude, Extract, NonNullable
  • Le pattern [T] extends [U] désactivé la distribution en enveloppant le type
  • never est une union vide : les conditional types distributifs sur never retournent never
  • 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

Sources

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