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

never & l'exhaustivité, ou le cas qui ne peut pas arriver never & exhaustiveness, or the case that cannot happen

Une union discriminée décrit les formes légales — mais le switch peut en oublier une, et compiler. Pire : ajoute un membre, et tous les switch déjà écrits l'ignorent en silence. never = « ce cas ne peut pas arriver » : un default qui assigne à never casse la compilation dès qu'une variante est oubliée. À ne pas saboter par un default fourre-tout qui rouvre le trou. A discriminated union describes the legal shapes — but the switch can forget one, and compile. Worse: add a member, and every switch already written ignores it silently. never = 'this case cannot happen': a default assigning to never breaks the build the moment a variant is forgotten. Don't sabotage it with a catch-all default that reopens the hole.

AudienceAudience
Dev qui modélise en unions discriminées et veut que l'oubli d'un cas casse le build Dev modeling with discriminated unions who wants a forgotten case to break the build
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●●○ neverExhaustivitéTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

L'union pose les formes. Mais rien ne t'oblige à toutes les traiter.The union lays down the shapes. But nothing forces you to handle them all.

C'est la porte laissée ouverte par le numéro précédent. Une union discriminée décrit exactement les formes légales — mais le switch qui les parcourt peut en oublier une, et compiler quand même. Pire : le jour où tu ajoutes un membre, tous les switch déjà écrits l'ignorent en silence. Le bug n'est pas à l'écriture du switch ; il naît six mois plus tard, à l'ajout d'une variante. never est l'outil qui transforme cet oubli en erreur de compilation.This is the door left open by the previous issue. A discriminated union describes exactly the legal shapes — but the switch that walks them can forget one, and still compile. Worse: the day you add a member, every switch already written ignores it silently. The bug isn't in writing the switch; it's born six months later, when a variant is added. never is the tool that turns that omission into a compile error.

Le cas oublié — silencieux, jusqu'au runtimeThe forgotten case — silent, until runtime
type Shape =
  | { kind: "circle"; r: number }
  | { kind: "rect"; w: number; h: number }
  | { kind: "triangle"; b: number; h: number };   // ← ajouté aujourd'hui

function area(s: Shape) {
  switch (s.kind) {
    case "circle": return Math.PI * s.r ** 2;
    case "rect":   return s.w * s.h;
    // triangle oublié : aucune erreur ICI — area est inférée number | undefined
  }
}   // 💥 l'erreur surgit loin, à l'usage — ou jamais : `${area(t)} m²` affiche « undefined m² »
Quand l'oubli est-il signalé ?When is the omission flagged?
Switch nuBare switch
jamais (ou un undefined plus loin)never (or an undefined later)
le bug naît à l'ajout d'un membrethe bug is born when a member is added
Garde nevernever guard
à la compilation, sur le cas manquantat compile time, on the missing case
impossible d'oublier sans casser le buildimpossible to forget without breaking the build

Sans garde, le compilateur te laisse oublier une branche — et le coût se paie à l'exécution, loin de la cause. Avec never, l'oubli devient un refus de compiler, pointé sur la variante non gérée.Without a guard, the compiler lets you forget a branch — and the cost is paid at runtime, far from the cause. With never, the omission becomes a refusal to compile, pointed at the unhandled variant.

La garde d'exhaustivité — l'oubli casse la compilationThe exhaustiveness guard — the omission breaks the build
function area(s: Shape): number {
  switch (s.kind) {
    case "circle":   return Math.PI * s.r ** 2;
    case "rect":     return s.w * s.h;
    case "triangle": return (s.b * s.h) / 2;
    default: {
      const _exhaustive: never = s;   // ✗ si un cas manque, s n'est PAS 'never'
      return _exhaustive;             //   → erreur de compilation, pointée sur le cas oublié
    }
  }
}
Le réflexe du numéroThe issue's reflex

« Si j'ajoute un membre à cette union, le compilateur me force-t-il à traiter le nouveau cas ? » Un switch sur un tag sans garde d'exhaustivité est une bombe à retardement — branche le default sur never et l'oubli devient impossible."If I add a member to this union, does the compiler force me to handle the new case?" A switch on a tag without an exhaustiveness guard is a time bomb — wire the default to never and the omission becomes impossible.

Le compagnon indispensable de l'union.The union's indispensable companion.

Le 3.1 rendait l'état illégal irreprésentable ; le 3.2 rend l'oubli d'un cas irréalisable. Les deux ensemble forment le cœur du volume : on conçoit les formes, puis on exige que chacune soit traitée — et le compilateur tient le registre à notre place.3.1 made the illegal state unrepresentable; 3.2 makes forgetting a case impossible. Together they form the heart of the volume: you design the shapes, then demand that each be handled — and the compiler keeps the ledger for you.

№ 02La surpriseThe surprise4 min

never est le type sans aucune valeur. Rien ne lui est assignable — et c'est toute l'astuce.never is the type with no value at all. Nothing is assignable to it — and that's the whole trick.

Le narrowing du Vol 2 rétrécit une union membre par membre. Quand toutes les branches d'un switch sont traitées, il ne reste plus rien à éliminer : dans le default, le type de la variable devient never — l'ensemble vide. Lui assigner never compile. Mais s'il reste un cas non traité, la variable garde ce membre, et never refuse de l'accueillir. L'erreur n'est pas un effet de bord : c'est exactement le signal qu'un cas a été oublié.Vol 2's narrowing shrinks a union member by member. When every branch of a switch is handled, there's nothing left to eliminate: in the default, the variable's type becomes never — the empty set. Assigning never to it compiles. But if a case remains unhandled, the variable keeps that member, and never refuses to receive it. The error isn't a side effect: it's precisely the signal that a case was forgotten.

Union épuisée → le default est inatteignableUnion exhausted → the default is unreachable
function area(s: Shape): number {
  switch (s.kind) {
    case "circle":   return Math.PI * s.r ** 2;
    case "rect":     return s.w * s.h;
    case "triangle": return (s.b * s.h) / 2;
    default:
      // les trois membres sont éliminés : ici s : never
      const _: never = s;   // ✓ compile — never est assignable à never
      return _;
  }
}

Chaque case élimine un membre. Au default, l'union n'a plus de membre vivant : son type est never. L'affectation const _: never = s passe, parce que la seule chose assignable à never, c'est never. Le compilateur confirme : cette branche ne peut pas arriver.Each case eliminates a member. At the default, the union has no living member left: its type is never. The assignment const _: never = s passes, because the only thing assignable to never is never. The compiler confirms: this branch cannot happen.

Le narrowing rétrécit une union jusqu'à never quand tout est épuisé. L'exhaustivité, c'est exiger d'atteindre ce never — et laisser le compilateur protester sinon.Narrowing shrinks a union down to never once all is exhausted. Exhaustiveness means demanding you reach that never — and letting the compiler protest otherwise.
№ 03La mécaniqueThe mechanics5 min

Une fonction qui n'accepte que l'impossible — et garde la porte aux deux étages.A function that accepts only the impossible — and guards the door on both floors.

On range la garde dans un helper : assertNever(x: never). Comme son paramètre est de type never, on ne peut l'appeler qu'avec une valeur que le narrowing a réduite à never — c'est-à-dire dans un default vraiment inatteignable. Si un cas manque, la valeur n'est pas never, et l'appel ne compile pas. Bonus : à l'exécution, si une valeur aberrante arrivait quand même, la fonction lève au lieu de continuer en silence.You stash the guard in a helper: assertNever(x: never). Since its parameter is typed never, you can only call it with a value narrowing has reduced to never — that is, in a truly unreachable default. If a case is missing, the value isn't never, and the call won't compile. Bonus: at runtime, should an aberrant value slip through anyway, the function throws instead of continuing silently.

Le helper — écrit une fois, posé partoutThe helper — written once, dropped everywhere
// Une seule fois dans le projet : la garde réutilisable.
function assertNever(x: never): never {
  throw new Error("Cas non géré : " + JSON.stringify(x));
}
L'usage — le default qui exige l'exhaustivitéThe use — the default that demands exhaustiveness
function label(s: Shape): string {
  switch (s.kind) {
    case "circle":   return "cercle";
    case "rect":     return "rectangle";
    case "triangle": return "triangle";
    default: return assertNever(s);   // ✓ compile SI tous les cas sont couverts
  }
}
// Ajoute { kind: "hexagon" } à Shape → cette ligne ne compile plus.
Les trois visages de neverThe three faces of never
Ce qu'il estWhat it is
Ensemble videEmpty setle type d'aucune valeurthe type of no value
Type bottomBottom typeassignable VERS tout, rien vers luiassignable TO everything, nothing to it
Anti-unknownAnti-unknownl'opposé du type topthe opposite of the top type
Où il surgitWhere it surfaces
Narrowing épuiséNarrowing exhaustedl'union réduite à zéro membrethe union shrunk to zero members
Ne retourne jamaisNever returnsretour de throw / boucle infiniereturn of throw / infinite loop
Filtre de typesType filteréliminer une branche d'un type conditionneldropping a branch of a conditional type
Les voisins, seulement nommés.The neighbors, only named.

Ne confonds pas never et void : void = « retourne, mais on ignore quoi » ; never = « ne retourne jamais ». Sans return devant assertNever, certains tsconfig exigent encore un retour : le return assertNever(s) sert les deux. Et never ressurgira au Vol 6 comme outil pour retirer des membres d'une union au niveau type.Don't confuse never with void: void = "returns, but we ignore what"; never = "never returns". Without return before assertNever, some tsconfig setups still demand a return value: return assertNever(s) serves both. And never will return in Vol 6 as a tool to strip members from a union at the type level.

№ 04Le piègeThe trap4 min

Le default fourre-tout qui « gère » tout — et rebouche le trou.The catch-all default that "handles" everything — and reseals the hole.

Le piège n'est pas dans never, il est dans le réflexe défensif qu'il combat. Un default qui renvoie une valeur de repli — « inconnu », 0, null — a l'air prudent : plus de crash. Mais il avale aussi tous les cas futurs. La valeur narrowée n'atteint jamais never, l'exhaustivité ne se déclenche pas, et ajouter un membre ne casse plus rien. Tu as échangé une erreur de compilation bruyante contre un bug silencieux — exactement le marché qu'on refusait.The trap isn't in never, it's in the defensive reflex it fights. A default that returns a fallback — 'unknown', 0, null — looks prudent: no more crash. But it also swallows every future case. The narrowed value never reaches never, exhaustiveness never fires, and adding a member breaks nothing. You've traded a loud compile error for a silent bug — exactly the deal we were refusing.

Un repli prudent en apparence, aveugle en pratiqueA seemingly prudent fallback, blind in practice
function label(s: Shape): string {
  switch (s.kind) {
    case "circle": return "cercle";
    case "rect":   return "rectangle";
    default: return "inconnu";   // ✗ avale TOUT le reste — y compris triangle
  }
}
// Ajoute { kind: "hexagon" } → toujours vert. Le default fourre-tout
// a rebouché le trou : l'oubli est redevenu silencieux.

Le default: return "inconnu" capture triangle et tout membre ajouté plus tard. La valeur n'atteint jamais never : le filet d'exhaustivité est débranché, sans le moindre avertissement. C'est le contre-exemple obligatoire : un default qui produit une valeur annule tout l'intérêt de never.The default: return "unknown" captures triangle and any member added later. The value never reaches never: the exhaustiveness net is unplugged, without a single warning. This is the mandatory counter-example: a default that produces a value cancels the whole point of never.

La limite : never ne valide pas la frontière.The limit: never doesn't validate the boundary.

L'exhaustivité protège le code typé, pas la donnée du dehors. Un kind: "pentagon" venu d'un JSON.parse traverse tous les case et tombe dans le default : assertNever lève alors (mieux qu'un undefined muet), mais ça reste un crash. La vraie défense au bord, c'est valider la donnée avant qu'elle entre — le Vol 7. never garde la porte intérieure, pas l'entrée.Exhaustiveness protects typed code, not outside data. A kind: "pentagon" from a JSON.parse walks past every case and lands in the default: assertNever then throws (better than a silent undefined), but it's still a crash. The real defense at the edge is to validate data before it enters — Vol 7. never guards the inner door, not the entrance.

Un default qui renvoie une valeur gère l'inconnu ; un default qui assertNever interdit l'inconnu. Le premier rassure, le second protège.A default that returns a value handles the unknown; a default that assertNevers forbids the unknown. The first reassures, the second protects.
№ 05Bilan & éditoWrap-up & editorial2 min

Le cas qui ne peut pas arriver — et le compilateur qui veille à ce qu'il n'arrive pas.The case that cannot happen — and the compiler that makes sure it doesn't.

never est l'envers d'any : le type sans aucune valeur, où le narrowing arrive quand toute une union est épuisée. En exigeant d'atteindre ce never dans le default, on force le compilateur à protester dès qu'un cas reste sur le carreau. L'union discriminée pose les formes ; never garde la porte, à l'écriture comme aux évolutions.never is any's mirror image: the type with no value, where narrowing lands once a whole union is exhausted. By demanding you reach that never in the default, you force the compiler to protest the moment a case is left out. The discriminated union lays down the shapes; never guards the door, at write time and through every change.

Tu veux…You want to…Le gesteThe moveCe que ça coûteWhat it costs
Attraper un cas oubliéCatch a forgotten caseUn default qui assigne à never (ou appelle assertNever).A default assigning to never (or calling assertNever).Un peu de cérémonie dans chaque switch exhaustif.A touch of ceremony in every exhaustive switch.
Documenter l'inatteignableDocument the unreachableLe type never : « cette branche ne peut pas arriver ».The never type: "this branch cannot happen".Il faut intégrer never = ensemble vide / type bottom.You must grasp never = empty set / bottom type.
Un double filetA double netassertNever casse la compilation et lève à l'exécution.assertNever breaks the build and throws at runtime.Ne remplace pas la validation à la frontière (Vol 7).Doesn't replace boundary validation (Vol 7).
Ne PAS l'employerNOT use itLe default fourre-tout qui renvoie une valeur de repli.The catch-all default returning a fallback value.Il rouvre le trou : ajouter un membre ne casse plus rien.It reopens the hole: adding a member breaks nothing.
Gérer l'inconnu, c'est l'admettre. Le déclarer impossible, c'est demander au compilateur de t'arrêter le jour où il cesse de l'être.Handling the unknown admits it. Declaring it impossible asks the compiler to stop you the day it stops being so.
Mot de l'éditeurFrom the editor

« Ce cas ne peut pas arriver. »"This case cannot happen."

On a tous écrit le commentaire « // ne devrait jamais arriver » au-dessus d'un default. C'est une prière, pas une preuve : le jour où le cas arrive — parce qu'on a ajouté une variante trois fichiers plus loin — le commentaire est toujours là, faux, et le code passe quand même. never transforme cette prière en théorème. Tant que l'union est complètement traitée, le default est de type never et tout va bien ; à la seconde où un membre échappe au switch, l'affectation à never ne compile plus, et l'erreur pointe précisément l'endroit à corriger. Le compilateur tient le registre des cas à notre place — et il ne l'oublie jamais.We've all written the '// should never happen' comment above a default. It's a prayer, not a proof: the day the case does happen — because a variant was added three files away — the comment is still there, false, and the code passes anyway. never turns that prayer into a theorem. As long as the union is fully handled, the default is of type never and all is well; the second a member escapes the switch, the assignment to never stops compiling, and the error points precisely at the spot to fix. The compiler keeps the ledger of cases for us — and it never forgets.

Avec le 3.1 et le 3.2, on sait rendre une mauvaise combinaison de formes irreprésentable, et un cas oublié impossible. Reste l'invariant niché dans une valeur primitive : un string qui est en vérité un Email validé, un number qui est un UserId, jamais l'inverse. Le structurel les rend interchangeables ; le prochain numéro — branded / opaque types — leur rend une identité que le compilateur défend. C'est le sommet du volume.With 3.1 and 3.2, we can make a bad combination of shapes unrepresentable, and a forgotten case impossible. What remains is the invariant tucked inside a primitive value: a string that's really a validated Email, a number that's a UserId, never the reverse. Structural typing makes them interchangeable; the next issue — branded / opaque types — gives them an identity the compiler defends. It's the volume's summit.