Vol. 1 · № 02 · Un atelier TypeScriptA TypeScript workshop

Unions & intersections, ou composer les formes Unions & intersections, or composing shapes

A | B, c'est l'une OU l'autre ; A & B, les deux à la fois. Et le contre-sens qui surprend tout le monde : sur une union, on ne peut toucher que ce que les deux formes ont en commun. Réunir des valeurs, c'est rétrécir les champs accessibles — la composition se lit à l'envers de l'intuition. A | B is one OR the other; A & B is both at once. And the twist that surprises everyone: on a union, you can only touch what both shapes share. Joining values narrows the accessible fields — composition reads backwards from intuition.

AudienceAudience
Dev à l'aise avec le typage structurel, prêt à composer des types Dev comfortable with structural typing, ready to compose types
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●○○○ UnionIntersectionTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

Une valeur, plusieurs formes possibles. Une seule à la fois.One value, several possible shapes. Exactly one at a time.

Le typage structurel décrit une forme. Mais le monde réel est fait d'alternatives : une réponse réussie ou en erreur, un identifiant texte ou numérique. L'union A | B note ça directement — « l'une de ces formes, exclusivement » — et c'est la brique de presque toute modélisation honnête.Structural typing describes one shape. But the real world is made of alternatives: a response that succeeded or failed, an id that's text or numeric. The union A | B writes that directly — 'one of these shapes, exclusively' — and it's the brick of almost any honest model.

Le code qu'on croit anodinThe code we think is harmless
type Ok  = { status: "ok";    data: string };
type Err = { status: "error"; message: string };

type Result = Ok | Err;   // l'une OU l'autre — jamais entre les deux

function code(r: Result) {
  return r.status;   // ✓ status existe dans les DEUX formes → toujours lisible
}
Que contient Result ?What does Result hold?
Ok
{ status, data }
|
Err
{ status, message }
⟶
une seule, à l'exécutionexactly one, at runtime

Un Result n'est jamais « un peu des deux ». À l'exécution, c'est soit un Ok, soit un Err — et status, présent dans les deux, reste toujours lisible.A Result is never "a bit of both". At runtime it's either an Ok or an Err — and status, present in both, stays always readable.

Le réflexe du numéroThe issue's reflex

« Cette donnée, c'est l'une OU l'autre forme (union), ou les deux à la fois (intersection) ? » Avant d'écrire un champ optionnel ou une classe de base, demande-toi laquelle des deux compositions décrit vraiment la réalité."Is this data one OR the other shape (union), or both at once (intersection)?" Before reaching for an optional field or a base class, ask which of the two compositions actually describes reality.

Pourquoi ce numéro suit le structurel.Why this issue follows structural typing.

Le numéro précédent a posé qu'un type est une forme. Celui-ci montre comment assembler ces formes — l'union pour les alternatives, l'intersection pour les cumuls. Tout le reste de TypeScript, du narrowing aux génériques, manipule ces deux opérateurs.The previous issue established that a type is a shape. This one shows how to assemble those shapes — union for alternatives, intersection for combinations. The rest of TypeScript, from narrowing to generics, manipulates these two operators.

№ 02La surpriseThe surprise4 min

Une union élargit les valeurs. Et rétrécit ce qu'on peut faire.A union widens the values. And narrows what you can do.

L'intuition dit : « string | number, donc je peux traiter un string OU un number, à ma guise ». Faux. Tant qu'on ne sait pas laquelle des deux on tient, TypeScript n'autorise que ce que les deux formes ont en commun. Réunir des possibilités, c'est restreindre les opérations sûres.Intuition says: 'string | number, so I can treat it as a string OR a number, as I please'. Wrong. Until you know which of the two you hold, TypeScript allows only what both shapes share. Joining possibilities restricts the safe operations.

Refusé — l'opération n'est sûre que pour une des deux formesRejected — the operation is safe for only one shape
function fmt(x: string | number) {
  return x.toUpperCase();
  //       ^^^^^^^^^^^
  // error: Property 'toUpperCase' does not exist on type 'string | number'.
  //        Property 'toUpperCase' does not exist on type 'number'.
}

toUpperCase existe sur string, pas sur number. Comme x pourrait être l'un ou l'autre, TypeScript refuse : il ne garantit que les membres présents dans toutes les branches de l'union. C'est exactement le crash x.toUpperCase is not a function qu'on évite.toUpperCase exists on string, not on number. Since x could be either one, TypeScript refuses: it only guarantees members present in every branch of the union. That's exactly the x.toUpperCase is not a function crash you avoid.

Une union ne te donne pas le choix des deux formes — elle t'oblige à prouver laquelle tu tiens avant d'agir.A union doesn't give you the run of both shapes — it forces you to prove which one you hold before acting.
№ 03La mécaniqueThe mechanics5 min

Réunir les valeurs, c'est croiser les champs. À l'envers de l'intuition.Joining the values intersects the fields. Backwards from intuition.

Voici le contre-sens qui surprend tout le monde. L'union des valeurs (A | B) donne l'intersection des champs accessibles ; l'intersection des valeurs (A & B) donne l'union des champs. Plus on autorise de valeurs, moins on peut toucher de champs sûrement — et inversement.Here's the twist that surprises everyone. Union of values (A | B) yields the intersection of accessible fields; intersection of values (A & B) yields the union of fields. The more values you allow, the fewer fields you can safely touch — and vice versa.

Deux opérateurs, deux directionsTwo operators, two directions
type A = { id: number; a: string };
type B = { id: number; b: boolean };

type U = A | B;   // accès : seulement { id } — le commun aux deux
type I = A & B;   // accès : { id; a; b } — tout des deux

declare const u: U;
u.id;   // ✓ présent dans A ET dans B
u.a;    // ✗ pas garanti — u pourrait être un B

declare const i: I;
i.id; i.a; i.b;   // ✓ i est A et B à la fois
Le croisementThe crossover
A | B
✓id
✗a
✗b
union des valeurs → intersection des champsunion of values → intersection of fields
A & B
✓id
✓a
✓b
intersection des valeurs → union des champsintersection of values → union of fields

Logique, en fait : si u peut être un B, lire u.a n'est pas sûr. Mais si i est à la fois A et B, alors i.a comme i.b existent forcément.It's logical, really: if u can be a B, reading u.a isn't safe. But if i is both A and B, then i.a and i.b necessarily exist.

La bonne questionThe right question

Avant de choisir l'opérateur : « est-ce que toute valeur sera les deux à la fois (alors &), ou l'une parmi plusieurs (alors |) ? » Confondre les deux, c'est soit un type trop permissif, soit un type impossible à satisfaire.Before picking the operator: "will every value be both at once (then &), or one among several (then |)?" Mixing them up yields either an over-permissive type or an unsatisfiable one.

Les voisins, seulement nommés.The neighbors, only named.

L'intersection sert surtout à cumuler des contrats (Props & { onClose }) — souvent là où l'héritage de classe servait ailleurs. Pour les unions d'objets, le discriminant (status littéral) en fait des discriminated unions : c'est le Vol 3.Intersection mostly serves to accumulate contracts (Props & { onClose }) — often where class inheritance served elsewhere. For object unions, a discriminant (a literal status) turns them into discriminated unions: that's Vol 3.

№ 04Le piègeThe trap4 min

Le & mis pour « fusionner ». Le sac d'optionnels mis pour « les cas ».The & used to "merge". The bag of optionals used for "the cases".

Deux pièges symétriques. D'abord, l'intersection appliquée à des types qui n'ont aucune valeur commune : le résultat est never, un type que rien ne satisfait. Ensuite, l'erreur la plus coûteuse : modéliser des alternatives avec un seul objet plein de champs optionnels — qui laisse exister tous les états illégaux.Two symmetric traps. First, intersection applied to types with no common value: the result is never, a type nothing satisfies. Then, the costliest mistake: modeling alternatives with a single object full of optional fields — which lets every illegal state exist.

Piège 1 — l'intersection impossibleTrap 1 — the impossible intersection
type Weird = string & number;        // never — aucune valeur n'est les deux
type Bad   = { x: number } & { x: string };
//     i.x est number & string = never : i.x ne peut JAMAIS être lu

Aucune valeur n'est à la fois un string et un number : leur intersection est never. Même chose pour deux champs x de types inconciliables. Quand un & donne never sans qu'on l'ait voulu, c'est presque toujours qu'on voulait un |.No value is both a string and a number: their intersection is never. Same for two x fields of irreconcilable types. When a & yields never unintentionally, you almost always wanted a |.

Piège 2 — alternatives mal modéliséesTrap 2 — alternatives modeled wrong
// L'anti-pattern : un sac d'optionnels
type Resp = { ok: boolean; data?: string; error?: string };

const r: Resp = { ok: true, error: "boom" };  // ✓ compile…
//   « réussi » ET porteur d'une erreur, sans data : un état illégal accepté

Avec un seul objet, data et error peuvent être présents ensemble, ou absents tous les deux. Le type décrit plus d'états que la réalité n'en autorise — et chaque état parasite est un if défensif à écrire, ou un bug.With one object, data and error can both be present, or both absent. The type describes more states than reality allows — and each spurious state is a defensive if to write, or a bug.

Un champ optionnel dit « parfois ». Une union dit « l'un de ceux-ci ». « Parfois » laisse passer l'impossible.An optional field says "sometimes". A union says "one of these". "Sometimes" lets the impossible through.
№ 05Bilan & éditoWrap-up & editorial2 min

Deux opérateurs. Ou, et et.Two operators. Or, and and.

Avec le structurel et ces deux opérateurs, on tient déjà l'essentiel de la modélisation. Le reste — narrowing, discriminants, génériques — n'est qu'une façon de manier des unions et des intersections avec plus de précision.With structural typing and these two operators, you already hold the essentials of modeling. The rest — narrowing, discriminants, generics — is just a way of handling unions and intersections with more precision.

Tu veux…You want to…Le gesteThe moveCe que ça coûteWhat it costs
Modéliser une alternativeModel an alternativeUnion : A | B. La valeur est l'une OU l'autre forme, exclusivement.Union: A | B. The value is one OR the other shape, exclusively.On ne peut toucher que les champs communs tant qu'on n'a pas prouvé la branche (narrowing).You can only touch shared fields until you've proven the branch (narrowing).
Cumuler des contratsCombine contractsIntersection : A & B. La valeur satisfait les deux formes à la fois.Intersection: A & B. The value satisfies both shapes at once.Des champs aux types inconciliables donnent never — un type que rien ne remplit.Fields with irreconcilable types yield never — a type nothing fulfills.
Rendre la branche lisibleMake the branch readableUn discriminant littéral commun (status: "ok") : la union devient discriminated. Vol 3.A shared literal discriminant (status: "ok"): the union becomes discriminated. Vol 3.Un champ-marqueur à maintenir sur chaque variante. Presque toujours rentable.A marker field to maintain on each variant. Almost always worth it.
Prouver quelle brancheProve which branchTester à l'exécution (typeof, in, le discriminant) pour rétrécir. Vol 2.Test at runtime (typeof, in, the discriminant) to narrow. Vol 2.Une vérification réelle à écrire — car le type, lui, s'efface à l'exécution.A real check to write — because the type itself vanishes at runtime.
Quand & donne never, le compilateur ne bugue pas. Il te dit : tu as demandé une valeur qui est deux choses incompatibles à la fois.When & yields never, the compiler isn't buggy. It tells you: you asked for a value that is two incompatible things at once.
Mot de l'éditeurFrom the editor

Modéliser, ce n'est pas tout permettre. C'est interdire le reste.Modeling isn't allowing everything. It's forbidding the rest.

Le réflexe venu du JavaScript dynamique, c'est d'élargir : un objet avec quelques champs en plus « au cas où », un booléen ici, un optionnel là. Chaque élargissement ouvre une porte à un état que la réalité n'autorise pas, et qu'il faudra ensuite vérifier à la main, partout. L'union prend le chemin inverse : elle énumère exactement les formes valides et ferme tout le reste. C'est moins de liberté à l'écriture, et beaucoup moins de doute à la lecture — chaque valeur est, par construction, l'une des possibilités prévues.The reflex from dynamic JavaScript is to widen: an object with a few extra fields 'just in case', a boolean here, an optional there. Every widening opens a door to a state reality doesn't allow, one you'll then have to check by hand, everywhere. The union takes the opposite path: it enumerates exactly the valid shapes and closes the rest. Less freedom when writing, far less doubt when reading — every value is, by construction, one of the foreseen possibilities.

Prochain volume : le type s'efface à l'exécution. Une union dit « l'une de ces formes » — mais à l'exécution, il n'y a plus de type pour le dire. Comment prouve-t-on, branche par branche, laquelle on tient ? C'est le narrowing, et c'est le cœur du Vol 2.Next volume: types are erased at runtime. A union says 'one of these shapes' — but at runtime, there's no type left to say it. How do you prove, branch by branch, which one you hold? That's narrowing, and it's the heart of Vol 2.