Blog · 2026-06-19Blog · 2026-06-19

Le narrowing en TypeScript, expliqué simplement Narrowing in TypeScript, explained simply

Comment TypeScript sait, après un if, que ta variable est forcément une string ? C'est le narrowing : la preuve qu'il construit branche par branche. Voici comment ça marche.How does TypeScript know, after an if, that your variable is definitely a string? That's narrowing: the proof it builds branch by branch. Here's how it works.

TypeScriptNarrowingTypesAnalyse de flot

Tu as une variable de type string | number. Tu écris un if, et soudain, dans le bloc, TypeScript la traite comme une string pure — sans que tu aies rien annoté. Ce n’est pas de la magie : c’est le narrowing, l’analyse de flot qui restreint un type au fil des conditions.

TypeScript lit tes conditions

Le compilateur suit l’exécution comme tu la lirais, et rétrécit le type dans chaque branche :

function format(x: string | number) {
  if (typeof x === "string") {
    return x.toUpperCase(); // ici x est string
  }
  return x.toFixed(2);      // ici x est forcément number
}

Dans le if, x est string. Après le return, la branche string est éliminée, donc x ne peut plus être que number. Le type devient une preuve qui se construit ligne après ligne.

Plusieurs gardes possibles

Le narrowing ne se limite pas à typeof. TypeScript comprend aussi :

// truthiness
if (user) { /* user n'est plus null | undefined */ }

// égalité
if (status === "ok") { /* status vaut littéralement "ok" */ }

// in
if ("wings" in animal) { /* animal a la propriété wings */ }

// instanceof
if (err instanceof TypeError) { /* err est un TypeError */ }

Chacune de ces formes est un indice que le compilateur sait interpréter pour réduire l’ensemble des types possibles.

Le piège : ce qui casse le narrowing

L’analyse de flot suit le code, mais elle peut le perdre. Un appel de fonction entre la garde et l’usage, par exemple, et TypeScript ne garantit plus rien :

function f(x: string | null) {
  if (x === null) return;
  doSomething();      // et si ça remettait x à null ?
  x.toUpperCase();    // x est encore string ici — TS le sait :
}                     // il parie qu'un appel ne réassigne pas une locale

Mais avec une propriété d’objet mutable, le narrowing est perdu après tout appel intermédiaire : TS ne peut pas prouver qu’elle n’a pas changé. La règle pratique : narrow sur des valeurs stables, copie dans une const si besoin.

Quand une seule garde ne suffit plus à distinguer plusieurs formes, on passe au cran au-dessus : les unions discriminées, où un champ tag rend chaque cas exclusif. C’est la suite naturelle du narrowing.

You have a variable of type string | number. You write an if, and suddenly, inside the block, TypeScript treats it as a pure string — without you annotating anything. It’s not magic: it’s narrowing, the control-flow analysis that restricts a type as conditions accumulate.

TypeScript reads your conditions

The compiler follows execution the way you’d read it, and narrows the type in each branch:

function format(x: string | number) {
  if (typeof x === "string") {
    return x.toUpperCase(); // here x is string
  }
  return x.toFixed(2);      // here x must be number
}

In the if, x is string. After the return, the string branch is gone, so x can only be number. The type becomes a proof built line by line.

Many possible guards

Narrowing isn’t limited to typeof. TypeScript also understands:

// truthiness
if (user) { /* user is no longer null | undefined */ }

// equality
if (status === "ok") { /* status is literally "ok" */ }

// in
if ("wings" in animal) { /* animal has the wings property */ }

// instanceof
if (err instanceof TypeError) { /* err is a TypeError */ }

Each of these forms is a clue the compiler knows how to read to shrink the set of possible types.

The trap: what breaks narrowing

Flow analysis follows the code, but it can lose track. A function call between the guard and the use, for instance, and TypeScript no longer guarantees anything:

function f(x: string | null) {
  if (x === null) return;
  doSomething();      // what if this set x back to null?
  x.toUpperCase();    // x is still string here — TS knows it:
}                     // it bets a call won't reassign a local

But with a mutable object property, narrowing is lost after any intermediate call: TS can’t prove it didn’t change. The practical rule: narrow on stable values, copy into a const if needed.

When a single guard no longer distinguishes several shapes, you step up a level: discriminated unions, where a tag field makes each case exclusive. It’s the natural sequel to narrowing.

Tous les articlesAll articles