Vol. 7 · № 01 · Un atelier TypeScriptA TypeScript workshop

Le mensonge de la frontière, ou d'où viennent les any The boundary's lie, or where the anys come from

strict activé, jamais un any sous tes doigts — et pourtant ton code en grouille. Ils entrent par les bords : JSON.parse rend any, fetch().json() rend Promise<any>, process.env reste non validé. L'annotation que tu poses dessus est un vœu, pas une preuve (Vol 2 + Vol 4). La donnée du dehors n'est typée qu'au point précis où un contrôle l'a prouvée — partout ailleurs, elle est supposée. strict on, never an any under your fingers — yet your code teems with them. They enter through the edges: JSON.parse returns any, fetch().json() returns Promise<any>, process.env stays unvalidated. The annotation you put on them is a wish, not a proof (Vol 2 + Vol 4). Outside data is typed only at the exact point a check proved it — everywhere else, it's assumed.

AudienceAudience
Dev au code « bien typé » qui voit des crashs de type surgir en production Dev with a 'well-typed' codebase seeing type crashes surface in production
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●○○ Frontièreany impliciteTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

L'annotation sur une donnée du dehors est un vœu, pas une preuve.The annotation on outside data is a wish, not a proof.

Tout ce qui entre dans le programme — réseau, JSON.parse, localStorage, process.env, formulaires — arrive non typé. Mais les signatures le cachent : on écrit const config: Config = JSON.parse(raw), le compilateur sourit, et l'on croit tenir un Config. On ne tient rien. Le type s'est effacé bien avant que les octets n'arrivent (Vol 2), et l'annotation n'a fait que blanchir un any (Vol 4). Ce volume paie la dette des deux : il nomme la frontière, et le seul endroit où la donnée devient vraiment typée.Everything entering the program — network, JSON.parse, localStorage, process.env, forms — arrives untyped. But the signatures hide it: you write const config: Config = JSON.parse(raw), the compiler smiles, and you think you hold a Config. You hold nothing. The type erased long before the bytes arrived (Vol 2), and the annotation merely laundered an any (Vol 4). This volume pays both debts: it names the boundary, and the one place where data truly becomes typed.

L'annotation optimiste — un Config qui n'en est peut-être pas unThe optimistic annotation — a Config that may not be one
interface Config { port: number; host: string }

const config: Config = JSON.parse(raw);   // ✗ annoté Config — mais RIEN ne l'a vérifié
config.port.toFixed();                     // 💥 si le JSON n'a pas 'port' : crash, loin d'ici
// JSON.parse renvoie 'any' : l'annotation est un vœu, pas une preuve (Vol 2 + Vol 4)
À quel point la donnée devient-elle typée ?At what point does the data become typed?
: Config = JSON.parse(…)
typé par décrettyped by decree
un vœu — personne n'a vérifiéa wish — nobody checked
: unknown, puis prouvé
typé après contrôletyped after a check
un fait — la preuve a eu lieua fact — the proof happened

Annoter, ce n'est pas vérifier. : Config sur un JSON.parse décrète un type que personne n'a constaté — la donnée ne devient un fait qu'au moment d'un contrôle à l'exécution, jamais avant.Annotating isn't checking. : Config on a JSON.parse decrees a type no one observed — the data becomes a fact only at a runtime check, never before.

La vérité du bord — unknown, jusqu'à preuve du contraireThe edge's truth — unknown, until proven otherwise
const data: unknown = JSON.parse(raw);   // la vérité : on ne SAIT pas ce que c'est

// impossible de toucher data.port sans prouver d'abord (Vol 4.2) :
if (isConfig(data)) data.port.toFixed();  // ✓ typé seulement APRÈS la preuve
// (le validateur en un geste, c'est le 7.2 — ici, on cesse de se mentir)
Le réflexe du numéroThe issue's reflex

« Cette donnée vient-elle de l'extérieur — et si oui, à quel point précis devient-elle typée, et qui l'a prouvé ? » Si la réponse est « une annotation » ou « un as », elle n'est pas typée : elle est supposée."Does this data come from outside — and if so, at what exact point does it become typed, and who proved it?" If the answer is "an annotation" or "an as", it isn't typed: it's assumed.

Pourquoi ce numéro ouvre le Vol 7.Why this issue opens Vol 7.

Ce volume règle la dette laissée par le Vol 2 (le type s'efface) et le Vol 4 (any/as blanchissent). Ce premier numéro est un diagnostic : il montre d'où viennent les bugs runtime — la frontière qui ment. Le suivant prescrit le remède : valider une seule fois, au bord, et n'avoir plus que du typé sûr à l'intérieur.This volume settles the debt left by Vol 2 (the type erases) and Vol 4 (any/as launder). This first issue is a diagnosis: it shows where runtime bugs come from — the lying boundary. The next prescribes the cure: validate once, at the edge, and have only safe-typed data inside.

№ 02La surpriseThe surprise4 min

Les any qui te plombent, tu ne les as pas écrits. Ils entrent par les bords.The anys that sink you, you didn't write. They enter through the edges.

strict activé, noImplicitAny en place, jamais un any sous tes doigts — et pourtant ton programme en grouille. Ils sont injectés par les fonctions de frontière elles-mêmes : JSON.parse rend any, fetch().json() rend Promise<any>, localStorage et les URLSearchParams rendent des chaînes brutes. Le mensonge est dans les définitions de types fournies. Ce sont là les vraies sources des bugs de type à l'exécution — pas le any qu'on s'interdit, mais ceux qu'on ne voit pas entrer.strict on, noImplicitAny in place, never an any under your fingers — and yet your program teems with them. They're injected by the boundary functions themselves: JSON.parse returns any, fetch().json() returns Promise<any>, localStorage and URLSearchParams return raw strings. The lie is in the supplied type definitions. These are the real sources of runtime type bugs — not the any you forbid yourself, but the ones you don't see come in.

Survole-les : voici ce qu'ils rendent vraimentHover them: here's what they really return
JSON.parse(raw)               // : any
await res.json()              // : any         (Promise<any>)
localStorage.getItem("k")     // : string | null   → JSON.parse → any
process.env.PORT              // : string | undefined  (traité comme string sûr)
new URLSearchParams(s).get()  // : string | null
(e.target as HTMLInputElement).value  // un 'as' pour deviner la cible

Chacune de ces entrées arrive any ou en chaîne brute — c'est fidèle au runtime (le réseau ne transporte pas de types, Vol 2), mais TypeScript a choisi any plutôt qu'unknown par confort. Résultat : la porte d'entrée est grande ouverte, et c'est la définition de type elle-même qui la tient ouverte.Each of these entries arrives as any or a raw string — that's faithful to runtime (the network carries no types, Vol 2), but TypeScript chose any over unknown for convenience. Result: the front door is wide open, and it's the type definition itself holding it open.

Tu n'écris pas any — tu l'importes, à chaque JSON.parse, chaque .json(), chaque getItem. Les vrais any naissent au bord, pas sous ton clavier.You don't write any — you import it, at every JSON.parse, every .json(), every getItem. The real anys are born at the edge, not under your keyboard.
№ 03La mécaniqueThe mechanics5 min

Le type ne traverse pas le fil. À la frontière, il est inventé, pas reçu.The type doesn't cross the wire. At the boundary, it's invented, not received.

La mécanique découle du Vol 2. À l'exécution, il n'y a que des octets : une chaîne JSON, un corps de réponse, une valeur de localStorage. Aucun type ne voyage avec eux — il s'est effacé à la compilation, de l'autre côté du fil. Donc quand JSON.parse rend une valeur, TypeScript ne peut pas connaître sa forme : il l'invente (any). Le système de types s'arrête net au bord. Dedans, tout est preuve et inférence ; dehors, c'est du runtime nu. La couture entre les deux, c'est la frontière.The mechanic follows from Vol 2. At runtime there are only bytes: a JSON string, a response body, a localStorage value. No type travels with them — it erased at compile time, on the other side of the wire. So when JSON.parse returns a value, TypeScript can't know its shape: it invents one (any). The type system stops dead at the edge. Inside, all is proof and inference; outside, it's bare runtime. The seam between the two is the boundary.

La couture — là où la preuve s'arrête, le runtime reprendThe seam — where proof stops, runtime resumes
// À l'exécution, la frontière ne transporte que des octets / des chaînes :
//
//   réseau ──▶ une chaîne brute ──▶ JSON.parse ──▶ valeur JS ──▶ TON code
//                                       ▲
//                         ICI le type est INVENTÉ (any) :
//                         il n'a jamais voyagé avec la donnée (Vol 2)
//
// Le système de types s'arrête au bord. Dedans : preuve. Dehors : runtime nu.
Le geste minimal : retyper l'entrée en unknownThe minimal move: retype the input as unknown
// Le type FOURNI ment ; le type HONNÊTE est unknown.
function parseJSON(raw: string): unknown {   // au lieu d'any
  return JSON.parse(raw);
}
// On retype la frontière en unknown → le mur du Vol 4.2 force la validation.
L'inventaire des frontièresThe boundary inventory
Type fourniGiven type
JSON.parseany
res.json()Promise<any>
getItemstring | null
process.env.Xstring | undefined
Type honnêteHonest type
forme réelle ?real shape?unknown
forme réelle ?real shape?Promise<unknown>
JSON dedans ?JSON inside?à validerto validate
contenu valide ?valid content?à validerto validate
Les voisins, seulement nommés.The neighbors, only named.

C'est le Vol 2 (effacement) qui explique pourquoi le type n'arrive pas, et le Vol 4 qui montre comment any/as blanchissent ensuite le mensonge. Le contrôle honnête, c'est le type guard du Vol 2.3, ou — mieux, au prochain numéro — un schéma qui valide et produit le type. La donnée typée et sûre qui en ressort porte un nom : un DTO. process.env est le seul type semi-honnête (il avoue le undefined), mais il ne dit rien du contenu.It's Vol 2 (erasure) that explains why the type doesn't arrive, and Vol 4 that shows how any/as then launder the lie. The honest check is the type guard of Vol 2.3, or — better, next issue — a schema that validates and produces the type. The safe-typed data that comes out has a name: a DTO. process.env is the only semi-honest type (it admits the undefined), but it says nothing of the content.

№ 04Le piègeThe trap4 min

« On est typés, donc on est sûrs. » Le build vert ne parle pas des données du dehors."We're typed, so we're safe." The green build says nothing about outside data.

Le piège n'est pas une ligne, c'est une fausse équation : couverture de types = sûreté à l'exécution. On active strict, on bannit any, on dessine de belles interfaces — et l'on conclut qu'on est protégé. Mais chaque point d'entrée reste une supposition non vérifiée. Le compilateur vert ne certifie que la cohérence interne du code ; il ne sait rien des octets qui franchiront la frontière en production. La vraie question n'est pas « ai-je des types ? » mais « où la donnée du dehors est-elle prouvée ? ».The trap isn't a line, it's a false equation: type coverage = runtime safety. You turn on strict, ban any, draw fine interfaces — and conclude you're protected. But every entry point stays an unchecked assumption. The green compiler only certifies the code's internal consistency; it knows nothing of the bytes that will cross the boundary in production. The real question isn't 'do I have types?' but 'where is outside data proven?'.

Un projet « bien typé » aux bords grands ouvertsA 'well-typed' project with wide-open edges
// strict: true, zéro 'any' explicite, des interfaces partout — et pourtant :
const user   = await fetch("/me").then(r => r.json());   // any
const draft  = JSON.parse(localStorage.getItem("draft")!); // any
const port   = Number(process.env.PORT);                  // NaN si absent/invalide

// trois frontières non gardées = trois crashs latents.
// Le build est vert. Il ne dit RIEN sur les octets qui arrivent.

Le pire offenseur : fetch().json() as ApiResponse — une interface + un as qui rebaptisent un any en mensonge typé. Zéro vérification ; le crash est seulement reporté au premier champ manquant, à des écrans de la cause. Le strict: true n'y change rien : il garde l'intérieur, pas la porte.The worst offender: fetch().json() as ApiResponse — an interface + an as that rename an any into a typed lie. Zero verification; the crash is merely deferred to the first missing field, screens from the cause. strict: true changes nothing: it guards the inside, not the door.

process.env, le faux ami.process.env, the false friend.

process.env.X est string | undefined — le seul type de frontière à demi honnête, puisqu'il avoue l'absence. Mais il ne dit rien du contenu : un PORT présent peut valoir "abc". On le « corrige » d'un ! ou d'un défaut sans jamais valider la valeur, et l'on a juste déplacé le mensonge du type vers le contenu. Présence ≠ validité.process.env.X is string | undefined — the only semi-honest boundary type, since it admits absence. But it says nothing of the content: a present PORT can be "abc". You "fix" it with a ! or a default without ever validating the value, and you've just moved the lie from the type to the content. Presence ≠ validity.

Le compilateur vert prouve que ton code est cohérent avec lui-même. Il ne promet rien sur les octets qui arrivent — cette promesse-là, c'est à toi de la faire tenir, au bord.The green compiler proves your code is consistent with itself. It promises nothing about the bytes that arrive — that promise is yours to keep, at the edge.
№ 05Bilan & éditoWrap-up & editorial2 min

La donnée du dehors arrive nue. Le type, ici, est une supposition.Outside data arrives bare. The type, here, is an assumption.

Tout ce qui entre — réseau, JSON.parse, localStorage, env, formulaires — est non typé : le type s'est effacé avant le fil (Vol 2). Les signatures le cachent en rendant any, et any/as blanchissent ensuite le mensonge (Vol 4). Les vrais any de ton code naissent là, au bord, pas sous ton clavier. Ce numéro ne corrige rien : il diagnostique. La donnée n'est typée qu'au point précis où un contrôle l'a prouvée — partout ailleurs, elle est supposée.Everything entering — network, JSON.parse, localStorage, env, forms — is untyped: the type erased before the wire (Vol 2). The signatures hide it by returning any, and any/as then launder the lie (Vol 4). Your code's real anys are born there, at the edge, not under your keyboard. This issue fixes nothing: it diagnoses. Data is typed only at the exact point where a check proved it — everywhere else, it's assumed.

Tu reçois…You receive…La véritéThe truthCe que ça coûteWhat it costs
Du réseau / JSON.parseFrom network / JSON.parseLe vrai type est unknown, pas ton interface.The real type is unknown, not your interface.any caché : annoter ne prouve rien (Vol 2).Hidden any: annotating proves nothing (Vol 2).
process.env / localStorageprocess.env / localStoragestring | undefined / string | null — contenu non validé.string | undefined / string | null — content unvalidated.Présence ≠ validité du contenu.Presence ≠ content validity.
Savoir où la preuve s'arrêteKnow where proof stopsCartographier ses frontières d'entrée.Map your input boundaries.Tout le reste du volume en dépend.The rest of the volume depends on it.
Ne PAS l'employerNOT use itinterface + as sur fetch().json().interface + as on fetch().json().Un any rebaptisé — le crash est juste reporté.A renamed any — the crash is merely deferred.
Un type sur une donnée du dehors ne la décrit pas — il parie sur elle. Le pari tient jusqu'au premier octet qui le dément, en production.A type on outside data doesn't describe it — it bets on it. The bet holds until the first byte that disproves it, in production.
Mot de l'éditeurFrom the editor

« D'où viennent les any. »"Where the anys come from."

C'est la chose la plus dure à intérioriser de toute la série : ton code est rempli d'any que tu n'as jamais écrits. Ils ne viennent pas d'un moment de paresse, mais des fonctions de frontière elles-mêmes — JSON.parse, .json(), getItem —, dont les signatures fournies mentent par confort. L'annotation que tu poses dessus n'est pas une preuve : le type s'était effacé avant que les octets n'arrivent (Vol 2), et le as ne fait que rebaptiser le trou (Vol 4). Tant qu'on n'a pas vu ça, on débogue des crashs de type loin de leur cause, sans comprendre pourquoi un code « entièrement typé » se brise au contact du réel.It's the hardest thing to internalize in the whole series: your code is full of anys you never wrote. They don't come from a lazy moment, but from the boundary functions themselves — JSON.parse, .json(), getItem — whose supplied signatures lie for convenience. The annotation you put on them isn't a proof: the type erased before the bytes arrived (Vol 2), and the as merely renames the hole (Vol 4). Until you've seen this, you debug type crashes far from their cause, without grasping why a 'fully typed' codebase breaks on contact with the real world.

Le réflexe qui ouvre tout le volume tient en une question, à poser à n'importe quelle valeur : vient-elle du dehors — et si oui, à quel point précis devient-elle typée, et qui l'a prouvé ? Au bord, la réponse honnête est unknown. Ce numéro s'arrête au diagnostic ; le suivant prescrit le remède, et c'est le cœur pratique de la série : parse, don't validate. Plutôt que vérifier puis caster (rouvrir le trou), on transforme la donnée inconnue en une donnée typée prouvée, en un seul geste, à la frontière. Un schéma comme source de vérité, et plus rien d'autre à croire sur parole.The reflex that opens the whole volume is one question, to ask of any value: does it come from outside — and if so, at what exact point does it become typed, and who proved it? At the edge, the honest answer is unknown. This issue stops at the diagnosis; the next prescribes the cure, and it's the practical heart of the series: parse, don't validate. Rather than check then cast (reopening the hole), you turn unknown data into proven typed data, in one move, at the boundary. A schema as the source of truth, and nothing else to take on faith.