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

Le narrowing, ou la preuve qui se construit branche par branche Narrowing, or the proof built branch by branch

Un x: string | number | null ne se caste pas — il se prouve. À chaque if, chaque typeof, chaque return anticipé, TypeScript rétrécit le type : l'union perd un membre, branche par branche, jusqu'à ce qu'il sache exactement ce qu'on tient. C'est la façon de reconstruire une preuve par-dessus des valeurs nues. An x: string | number | null isn't cast — it's proven. At each if, each typeof, each early return, TypeScript narrows the type: the union loses a member, branch by branch, until it knows exactly what you hold. It's how a proof is rebuilt on top of bare values.

AudienceAudience
Dev qui a compris l'effacement et veut décider sûrement à l'exécution Dev who grasped erasure and wants to decide safely at runtime
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●○○ NarrowingControl flowTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

L'union ne se caste pas. Elle rétrécit, garde après garde.The union isn't cast. It narrows, gate after gate.

Le 2.1 a montré qu'à l'exécution il ne reste que des valeurs. Voici comment le compilateur, lui, garde le fil : il lit nos tests ordinaires — un if, un typeof, un return anticipé — et rétrécit le type en conséquence. Le même x n'a pas le même type d'une ligne à l'autre. C'est le narrowing, et c'est la signature de la série.2.1 showed that only values remain at runtime. Here's how the compiler keeps the thread: it reads our ordinary tests — an if, a typeof, an early return — and narrows the type accordingly. The same x doesn't have the same type from one line to the next. That's narrowing, and it's the series' signature.

Le même x, un type par ligneThe same x, one type per line
function len(x: string | string[] | null): number {
  if (x === null) return 0;            // ici x : string | string[]
  if (typeof x === "string")           // ici x : string
    return x.length;
  return x.length;                     // ici x : string[]
}
L'union qui rétrécitThe union shrinking
stringstring[]null
↓
if (x === null) returnif (x === null) return
stringstring[]null
↓
if (typeof x === "string")if (typeof x === "string")
vraitrue
string
sinonelse
string[]

Chaque garde élimine un membre. L'union de départ se réduit, branche par branche, jusqu'à un type unique — assez précis pour qu'on puisse enfin agir sans risque.Each gate eliminates a member. The starting union shrinks, branch by branch, down to a single type — precise enough to finally act without risk.

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

« Quelle preuve ai-je déjà apportée à ce point du code ? » Le type d'une variable n'est pas figé à sa déclaration : il suit le flot. Avant d'atteindre un cast, demande-toi quel test rétrécirait le type pour toi."What proof have I already established at this point in the code?" A variable's type isn't fixed at its declaration: it follows the flow. Before reaching for a cast, ask which test would narrow the type for you.

Pourquoi ce numéro suit l'effacement.Why this issue follows erasure.

Le narrowing est la réponse directe à l'effacement : puisqu'on ne peut pas interroger un type, on teste une valeur — et le compilateur traduit ce test en une preuve sur le type. C'est le pont qui relie le monde des valeurs (l'exécution) au monde des types (la compilation).Narrowing is the direct answer to erasure: since you can't interrogate a type, you test a value — and the compiler turns that test into a proof about the type. It's the bridge linking the world of values (runtime) to the world of types (compile time).

№ 02La surpriseThe surprise4 min

Pas de syntaxe magique. TypeScript lit le if que tu écris déjà.No magic syntax. TypeScript reads the if you already write.

Venant du JavaScript, on croit qu'il faudra « convaincre » le compilateur à coups de as. C'est l'inverse : le narrowing n'a aucune syntaxe propre. Il s'appuie sur le flot de contrôle ordinaire — tes if, tes return, tes &&. Écris le test qu'un humain écrirait pour se protéger, et le type suit tout seul.Coming from JavaScript, you assume you'll have to 'convince' the compiler with as. It's the opposite: narrowing has no syntax of its own. It rides on ordinary control flow — your ifs, your returns, your &&. Write the test a human would write to stay safe, and the type follows on its own.

Le cast qui ne vérifie rienThe cast that checks nothing
function shout(x: string | null) {
  // le réflexe « je sais mieux » : caster partout
  return (x as string).toUpperCase();
  // ✓ compile — mais si x est null, 💥 à l'exécution.
  // Le as a fait taire le compilateur, pas vérifié null.
}

as string ne retire pas le null : il ordonne au compilateur de l'ignorer. Le test qui aurait protégé n'a pas été écrit, juste contourné. On a rétabli exactement le crash que le type signalait.as string doesn't remove the null: it orders the compiler to ignore it. The test that would have protected you was never written, only bypassed. You've restored exactly the crash the type was flagging.

Un as dit au compilateur de fermer les yeux. Un if lui apprend quelque chose de vrai — et il s'en souvient.An as tells the compiler to look away. An if teaches it something true — and it remembers.
№ 03La mécaniqueThe mechanics5 min

Six gardes, et un compilateur qui se souvient.Six gates, and a compiler that remembers.

Le narrowing n'a pas de syntaxe à lui, mais il a un vocabulaire : une poignée de tests que le compilateur sait interpréter. Chacun retire un ensemble de possibilités. Et l'analyse de flot tient les comptes d'un bout à l'autre du bloc — ce qui est éliminé dans une branche reste éliminé dans la suite.Narrowing has no syntax of its own, but it has a vocabulary: a handful of tests the compiler knows how to read. Each removes a set of possibilities. And control flow analysis keeps the books across the whole block — what's eliminated in one branch stays eliminated afterward.

Le vocabulaire du narrowingThe vocabulary of narrowing
typeof x === "string"      // primitives : string, number, boolean, …
x instanceof Date          // classes (une vraie valeur JS)
"role" in obj              // présence d'une propriété
Array.isArray(x)           // tableau vs reste
if (x) { … }               // truthiness : retire null, undefined, 0, "", false
x.kind === "circle"        // égalité sur un discriminant littéral
Chaque garde, ce qu'elle prouveEach gate, what it proves
typeof x === …une primitive (string, number, boolean…)a primitive (string, number, boolean…)
x instanceof Cune instance de la classe Can instance of class C
"k" in objles variantes qui ont la propriété kthe variants that have property k
Array.isArray(x)le cas tableauthe array case
if (x) / x === vtruthiness, ou égalité à un littéral (discriminant)truthiness, or equality to a literal (discriminant)
isFoo(x)un type guard sur mesure — 2.3a custom type guard — 2.3
Le type qui se souvientThe type that remembers
function f(x: string | number) {
  if (typeof x === "number") {
    x;            // number
    return x + 1;
  }
  x;              // string — le compilateur SE SOUVIENT que le number est parti
  return x.trim();
}

Après un if qui return, le membre testé ne peut plus être présent dans la suite : l'analyse de flot le retire pour le reste du bloc. C'est pour ça que x est string après le bloc number, sans nouveau test — le compilateur a tenu le compte.After an if that returns, the tested member can't appear afterward: control flow analysis removes it for the rest of the block. That's why x is string after the number block, with no new test — the compiler kept the count.

À vous — le vrai compilateur répondYour turn — the real compiler answers

Tout ce qui précède se lit en hochant la tête et ne s'applique jamais : une règle n'a de poids que le jour où on la voit se déclencher. Alors écrivez. Le compilateur ci-dessous n'est pas une imitation — c'est tsc lui-même, chargé dans la page, avec les mêmes drapeaux strict que le site. Les codes qu'il rend sont ceux que vous reverrez dans votre terminal.Everything above reads with a nod and never gets applied: a rule only carries weight the day you watch it fire. So write. The compiler below isn't an imitation — it's tsc itself, loaded into the page, with the same strict flags as this site. The codes it returns are the ones you'll see again in your terminal.

`etiquette` reçoit `string | number | null` et doit rendre une `string` en majuscules — `"42"` pour un nombre, `"—"` pour `null`. Le compilateur refuse `.toUpperCase()` : à ce point du programme, il ne sait pas encore ce qu'il tient. Construisez la preuve au lieu de l'affirmer.`etiquette` takes `string | number | null` and must return an uppercase `string` — `"42"` for a number, `"—"` for `null`. The compiler refuses `.toUpperCase()`: at that point it doesn't yet know what it holds. Build the proof instead of asserting it.

portes de sortieescape hatches
Compiler ne suffit pasCompiling isn't enough
·Aucune porte de sortie : ni `as`, ni `any`, ni `!`, ni `@ts-ignore`No escape hatch: no `as`, no `any`, no `!`, no `@ts-ignore`
·La lecture de `.toUpperCase` reste dans le programmeThe read of `.toUpperCase` stays in the program
·Le flot est réellement découpé : au moins deux gardesThe flow is actually split: at least two guards

Un `as` fait toujours passer au vert — et ne prouve rien. C'est ce que ces lignes-là vérifient, sur l'arbre du programme et pas sur son texte.An `as` always turns the screen green — and proves nothing. That's what these lines check, on the program's tree rather than its text.

La colonne de droite ne compte pas les erreurs : elle compte les portes de sortie. C'est la seule chose qu'un « ✓ ça compile » ne saurait pas dire.The right-hand column doesn't count errors: it counts escape hatches. It's the one thing a “✓ it compiles” could never tell you.

Le voisin, seulement nommé.The neighbor, only named.

Quand l'union finit vide — tous les membres éliminés — le type devient never. C'est le ressort de la vérification d'exhaustivité : faire échouer la compilation si l'on oublie un cas. On le construit au Vol 3.2.When the union ends up empty — every member eliminated — the type becomes never. That's the spring of exhaustiveness checking: failing the build if you forget a case. We build it in Vol 3.2.

№ 04Le piègeThe trap4 min

La truthiness mange le 0. Et le narrowing s'oublie.Truthiness eats the 0. And narrowing forgets itself.

Deux façons de se faire piéger par un outil pourtant juste. La première : tester la « truthiness » pour dire « a une valeur » — mais 0, la chaîne vide et false sont falsy, et passent à la trappe. La seconde : croire qu'un type rétréci le reste pour toujours — alors qu'il se perd dès qu'une fonction pourrait avoir changé la donnée entre-temps.Two ways to get trapped by an otherwise-correct tool. First: testing 'truthiness' to mean 'has a value' — but 0, the empty string and false are falsy, and fall through. Second: assuming a narrowed type stays narrowed forever — when it's lost the moment a function could have changed the data in between.

Piège 1 — la truthiness trop largeTrap 1 — truthiness too broad
function label(count: number | undefined) {
  if (!count) return "rien";   // ⚠ piège : 0 est falsy
  return `${count} éléments`;
}
label(0);   // "rien" — alors que 0 est une valeur parfaitement valide
// truthiness retire undefined… mais aussi 0, "", NaN, false

!count est vrai pour undefined et pour 0. Pour ne retirer que l'absence, on teste explicitement : count === undefined, ou count == null (qui couvre null et undefined). À noter aussi : typeof null === "object" — la garde typeof x === "object" laisse passer null.!count is true for undefined and for 0. To remove only absence, test explicitly: count === undefined, or count == null (covering null and undefined). Note too: typeof null === "object" — the typeof x === "object" gate lets null through.

Piège 2 — le narrowing perduTrap 2 — narrowing lost
function f(o: { v: string | null }) {
  if (o.v !== null) {
    // ici o.v : string
    setTimeout(() => {
      o.v.toUpperCase();   // ✗ 'o.v' is possibly 'null'.
      // entre le test et l'appel, o.v a pu changer : le narrowing est perdu
    });
  }
}

Le narrowing d'une propriété mutable ne tient pas à travers une closure : le callback s'exécutera plus tard, et le compilateur doit supposer que o.v a pu redevenir null entre-temps. (Un simple appel de fonction, lui, ne l'efface pas — TypeScript fait là un pari optimiste.) Il refuse, à raison.Narrowing of a mutable property doesn't hold across a closure: the callback runs later, and the compiler must assume o.v could have gone back to null in between. (A plain function call doesn't erase it — TypeScript makes an optimistic bet there.) It refuses, rightly.

Le narrowing tient tant que la valeur ne peut pas changer. Un const — ou un let qu'on ne réassigne plus — fige la preuve ; une propriété, dans une closure, la laisse filer.Narrowing holds as long as the value can't change. A const — or a let never reassigned again — pins the proof; a property, inside a closure, lets it slip.
№ 05Bilan & éditoWrap-up & editorial2 min

Tester une valeur, prouver un type.Test a value, prove a type.

Le narrowing est la couture entre les deux mondes du Vol 2 : on teste ce qui reste à l'exécution — une valeur — et le compilateur en déduit une preuve sur ce qui s'est effacé — le type. Bien manié, il rend les casts presque inutiles.Narrowing is the seam between Vol 2's two worlds: you test what remains at runtime — a value — and the compiler infers a proof about what was erased — the type. Wielded well, it makes casts almost unnecessary.

Tu veux distinguer…You want to tell apart…La gardeThe gateLe piège à connaîtreThe gotcha to know
Une primitiveA primitivetypeof x === "string"typeof x === "string"typeof null === "object" : le null se glisse dans le cas objet.typeof null === "object": null sneaks into the object case.
Une classeA classx instanceof Date, Array.isArray(x)x instanceof Date, Array.isArray(x)Ne marche pas sur les interfaces ni les types nus — eux sont effacés.Doesn't work on interfaces or bare types — those are erased.
Une varianteA variantÉgalité sur un discriminant : x.kind === "circle".Equality on a discriminant: x.kind === "circle".Il faut avoir posé le champ discriminant — c'est le Vol 3.You must have placed the discriminant field — that's Vol 3.
« A une valeur »"Has a value"x != null (et non if (x)).x != null (not if (x)).La truthiness mange aussi 0, "" et false. Et le narrowing se perd à travers une closure.Truthiness also eats 0, "" and false. And narrowing is lost across a closure.
« 'o.v' is possibly 'null' » après un test réussi n'est pas un bug du compilateur. Il te dit : entre ta preuve et ton usage, la valeur a pu changer."'o.v' is possibly 'null'" after a passing test isn't a compiler bug. It tells you: between your proof and your use, the value could have changed.
Mot de l'éditeurFrom the editor

Le compilateur ne devine pas. Il te lit.The compiler doesn't guess. It reads you.

Le narrowing a quelque chose de presque troublant la première fois : on écrit un if tout bête, et soudain le type change tout seul dans le bloc. Il n'y a pourtant aucune magie — seulement un compilateur qui suit le même raisonnement qu'un lecteur humain : « si on est passé par ce return, alors ce cas est derrière nous ». C'est exactement pour ça que le bon réflexe n'est jamais le cast, mais le test : un as ment au compilateur, un if lui apprend une vérité qu'il propage pour nous. Écrire du TypeScript sûr, c'est en grande partie donner au compilateur les preuves qu'il sait déjà lire.Narrowing is almost unsettling the first time: you write a plain if, and suddenly the type changes by itself inside the block. Yet there's no magic — only a compiler following the same reasoning a human reader does: 'if we got past this return, that case is behind us'. That's exactly why the right reflex is never the cast, but the test: an as lies to the compiler, an if teaches it a truth it propagates for us. Writing safe TypeScript is, in large part, handing the compiler the proofs it already knows how to read.

Prochain numéro : les type guards et les assertion functions. Quand le test devient trop gros pour un if — vérifier qu'une donnée du dehors a bien la forme attendue — on l'emballe dans une fonction qui rend un x is Foo. C'est la seule façon de faire voyager une preuve de narrowing à travers une frontière de fonction.Next issue: type guards and assertion functions. When the test grows too big for an if — checking that outside data really has the expected shape — you wrap it in a function returning x is Foo. It's the only way to carry a narrowing proof across a function boundary.