Une seule tension du modèle TypeScript,
disséquée jusqu'à l'os. One tension in TypeScript's model,
dissected to the bone.
Une collection éditoriale pour le développeur qui vient du JavaScript dynamique et ne veut pas grinder la syntaxe. Un numéro = un mécanisme-clé, disséqué jusqu'à ce que la tension soit nue : quel bug il rend impossible, ce qu'il coûte, et quand ne pas l'employer. On ne fait plus taire le compilateur — on l'écoute énoncer un théorème. An editorial collection for the developer coming from dynamic JavaScript who refuses to grind syntax. One issue = one key mechanism, dissected until the tension is bare: which bug it makes impossible, what it costs, and when not to use it. You stop silencing the compiler — you hear it state a theorem.
Progression de lectureReading progress
0 / 27 numéros lusissues read
0 / 135 chapitreschapters
Suivie dans ce navigateur, sans compte.Tracked in this browser, no account needed.
Libre, sans compte ni paywall — Free, no account, no paywall — soutenir sur Ko-fi ☕support it on Ko-fi ☕
Vol. I Le typage structurelStructural typing
Le nom, ou la forme ?By name, or by shape? · 3 numérosissuesou la forme avant le nom Structural typing,
or shape before name
Le code naïf cherche un implements et ne le trouve pas : let n: Named = new Dog(). En TypeScript, ce qui rend deux types compatibles, ce n'est pas leur nom — c'est leur forme. Tout objet qui a les bons champs passe, qu'il ait entendu parler du type ou non. Une seule règle, et tout le modèle de TypeScript se met à pencher. Naive code looks for an implements and can't find it: let n: Named = new Dog(). In TypeScript, what makes two types compatible isn't their name — it's their shape. Any object with the right fields passes, whether it ever heard of the type or not. One rule, and all of TypeScript's model starts to tilt.
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.
ou la fraîcheur d'un littéral Excess property checks,
or a literal's freshness
Le typage structurel accepte les champs en trop — sauf un cas. Un littéral d'objet « frais », assigné directement, voit ses propriétés inconnues refusées. Ce n'est pas une garantie de structure : c'est un linter cousu dans le type, qui attrape le isAdmin mal tapé. Et il s'évapore dès qu'on passe par une variable. Structural typing accepts extra fields — except in one case. A 'fresh' object literal, assigned directly, has its unknown properties rejected. It's not a structural guarantee: it's a linter sewn into the type, catching the mistyped isAdmin. And it vanishes the moment you go through a variable.
Vol. II Le type s'effaceTypes are erased
Ça survit à l'exécution ?Does it survive at runtime? · 3 numérosissuesou la preuve qui s'évapore Type erasure,
or the proof that evaporates
const u: User = JSON.parse(x) compile sans broncher — puis u.name plante. L'annotation : User n'a rien vérifié : à l'exécution, elle n'existe plus. Les types prouvent à la compilation, puis s'effacent. Tout le reste de la série tient dans cette phrase. const u: User = JSON.parse(x) compiles without a blink — then u.name throws. The : User annotation verified nothing: at runtime, it no longer exists. Types prove at compile time, then erase. The rest of the series fits inside that sentence.
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.
ou la preuve qui traverse une fonction Type guards & assertion functions,
or the proof that crosses a function
Sortir un test dans une fonction, et le narrowing disparaît : un boolean ne dit pas QUOI on a prouvé. Le prédicat x is Foo le fait voyager — mais le compilateur ne vérifie jamais que le corps tient la promesse. Un guard qui ment compile. C'est un as déguisé, en pire. Extract a test into a function, and narrowing vanishes: a boolean doesn't say WHAT you proved. The x is Foo predicate carries it across — but the compiler never checks the body keeps the promise. A lying guard compiles. It's an as in disguise, but worse.
Vol. III Rendre l'illégal irreprésentableMake illegal states unrepresentable
Cet état, irreprésentable ?This state, unrepresentable? · 3 numérosissuesou l'une de ces formes,
exclusivement Discriminated unions,
or one of these shapes,
exclusively
Le sac de champs optionnels laisse construire « en cours, réussi ET en erreur » — 8 combinaisons, 5 absurdes, à exclure à la main. L'union discriminée ne décrit que les formes réelles : un discriminant littéral les sépare, l'état illégal ne se construit plus, et le narrowing du Vol 2 s'applique gratuitement. À une condition : que le tag soit littéral, jamais string. The bag of optional fields lets you build 'loading, succeeded AND errored' — 8 combinations, 5 absurd, to exclude by hand. The discriminated union describes only the real shapes: a literal discriminant separates them, the illegal state can't be built, and Vol 2's narrowing applies for free. On one condition: the tag must be literal, never string.
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.
ou un string qui a fait ses preuves Branded / opaque types,
or a string that earned its name
Le structurel rend un Email validé et un string brut interchangeables : sendTo(string) accepte tout. Le brand encode l'invariant validé dans le type — un champ fantôme qui rend Email distinct de string, qu'on n'obtient qu'en passant par un smart constructor. Coût nul à l'exécution (le brand s'efface), à la seule condition de confiner le as. À ne pas brander partout. Structural typing makes a validated Email and a raw string interchangeable: sendTo(string) accepts anything. The brand encodes the validated invariant into the type — a phantom field making Email distinct from string, obtained only through a smart constructor. Zero runtime cost (the brand erases), on the sole condition you confine the as. Don't brand everything.
Vol. IV `any`, `unknown` & l'échappatoire`any`, `unknown` & the escape hatch
Tu connais le type, ou tu abandonnes ?Do you know the type, or give up? · 3 numérosissuesle trou,
ou éteindre la lumière any,
the hole,
or turning off the lights
On met any, l'erreur disparaît, on passe à autre chose. Mais any n'est pas un type : c'est l'interrupteur qui débranche le vérificateur. La faute de frappe, le champ absent, la méthode inventée — tout passe, et le bug revient intact au runtime. Pire : any déteint, il recontamine en silence tout le code en aval. C'est l'unsafe de TypeScript. You slap on any, the error vanishes, you move on. But any isn't a type: it's the switch that unplugs the checker. The typo, the missing field, the invented method — all pass, and the bug returns intact at runtime. Worse: any bleeds, silently re-contaminating all the downstream code. It's TypeScript's unsafe.
l'inconnu honnête,
ou je ne sais pas,
alors prouve-le unknown,
the honest unknown,
or I don't know,
so prove it
unknown dit la même chose qu'any — « je ne connais pas ce type » — mais ferme la porte à clé au lieu de l'arracher. Tout monte dans unknown, rien n'en redescend sans preuve : impossible de lire, d'appeler, d'assigner, sans avoir d'abord rétréci. C'est le type top, miroir du never. Le défaut honnête au catch et à la frontière — à condition de le franchir par un check, jamais par un as. unknown says what any says — 'I don't know this type' — but locks the door instead of tearing it off. Everything flows up into unknown, nothing flows back down without proof: you can't read, call, or assign before narrowing. It's the top type, the mirror of never. The honest default in catch and at the boundary — provided you cross it by a check, never an as.
ou tais-toi,
je sais mieux as & assertions,
or be quiet,
I know better
Venant de Java/C#, on lit as comme un cast. Faux : il ne convertit rien, ne vérifie rien, ne touche pas la valeur. Il change juste ce que le compilateur croit — un contrat qu'on signe à sa place. Légitime quand on détient un fait qu'il ne peut pas connaître ; mensonge quand on fait taire une erreur qu'on n'a pas comprise. Sur un JSON.parse, ce n'est qu'un any rebaptisé. Coming from Java/C#, you read as as a cast. Wrong: it converts nothing, checks nothing, doesn't touch the value. It only changes what the compiler believes — a contract you sign in its place. Legitimate when you hold a fact it can't know; a lie when you silence an error you didn't understand. On a JSON.parse, it's just a renamed any.
Vol. V Les génériquesGenerics
Tous les types, ou une famille ?All types, or a constrained family? · 3 numérosissuesou une relation,
pas une inconnue Generics & inference,
or a relation,
not an unknown
Un générique n'est pas « un type qu'on ignore » — ça, c'est unknown. C'est une relation : <T> relie un paramètre au retour, deux arguments entre eux. Là où any rompt le lien et débranche la vérification, <T> garde le type et le fait voyager — inféré au site d'appel, sans rien écrire. Mais un T qui n'apparaît qu'une fois ne relie rien : c'est un as déguisé en chevrons. A generic isn't 'a type you don't know' — that's unknown. It's a relation: <T> ties a parameter to the return, two arguments together. Where any severs the link and unplugs checking, <T> keeps the type and carries it across — inferred at the call site, nothing written. But a T that appears only once relates nothing: it's an as dressed up in brackets.
ou de quelle capacité ai-je besoin ? Constraints (extends) & defaults,
or which capability do I need?
Sur un <T> libre, .length ne compile pas — et c'est correct : on ignore tout de ses capacités. La liberté de T est une impuissance. T extends { length: number } le borne à exactement la forme dont le corps se sert, débloque cette capacité, et garde le type exact à la sortie. keyof et T[K] relient clés et valeurs. Tout l'art : contraindre juste assez, ni any déguisé, ni carcan. On a free <T>, .length won't compile — and that's correct: you know nothing of its capabilities. The freedom of T is a powerlessness. T extends { length: number } bounds it to exactly the shape the body uses, unlocks that capability, and keeps the exact type at the output. keyof and T[K] relate keys and values. The whole art: constrain just enough, neither any in disguise nor a straitjacket.
ou dans quel sens va la flèche ? Variance,
or which way does the arrow go?
Cat est un sous-type d'Animal. Mais un Cat[] est-il un Animal[] ? Oui (covariance), pas l'inverse. Et un handler d'Animal passe pour un handler de Cat — la flèche s'inverse (contravariance), et c'est ce qui rend la substitution sûre. Sorties covariantes, entrées contravariantes. Avec deux trous assumés : tableaux mutables covariants, paramètres de méthode bivariants. Cat is a subtype of Animal. But is a Cat[] an Animal[]? Yes (covariance), not the reverse. And an Animal handler stands in for a Cat handler — the arrow flips (contravariance), and that's what makes substitution safe. Outputs covariant, inputs contravariant. With two owned holes: covariant mutable arrays, bivariant method parameters.
Vol. VI Le type au niveau typeType-level programming
Tu le calcules, ou tu le réécris ?Compute it, or rewrite it? · 3 numérosissuesou calculer plutôt que recopier Mapped types & keyof,
or compute rather than copy
Une variante d'un type réécrite à la main diverge de sa source : ajoute un champ, la copie ment. Le mapped type la calcule — [K in keyof T] parcourt les clés et les réécrit. Partial, Readonly, Record ne sont pas magiques : ce sont des boucles d'une ligne. Le prix, qui montera : dériver par confort couple ce qui ne devrait pas l'être. A type's variant rewritten by hand drifts from its source: add a field, the copy lies. The mapped type computes it — [K in keyof T] walks the keys and rewrites them. Partial, Readonly, Record aren't magic: they're one-line loops. The price, which will rise: deriving for convenience couples what shouldn't be.
ou brancher et extraire Conditional types & infer,
or branch and extract
Le mapped type transforme chaque clé pareil ; pour décider, il faut le if du langage des types : T extends U ? X : Y. infer R capture un type dans le motif — c'est ainsi qu'on extrait le retour d'une fonction (ReturnType). Sur une union, le test se distribue membre par membre, et never s'évapore : voilà Exclude. La puissance, et son prix : la pyramide de conditionnels que plus personne ne lit. The mapped type transforms every key alike; to decide, you need the type language's if: T extends U ? X : Y. infer R captures a type in the pattern — that's how you extract a function's return (ReturnType). Over a union, the test distributes member by member, and never evaporates: that's Exclude. The power, and its price: the conditional pyramid no one reads anymore.
ou quand s'arrêter Template literal types & utilities,
or when to stop
Le sommet du langage des types : composer des types à partir de chaînes — `on${Capitalize<K>}` —, multiplier des unions, parser avec infer. La synthèse du Vol 6, et son frein. La puissance est telle qu'on encode l'illisible sans s'en rendre compte. La seule question : ce type calculé m'achète-t-il une garantie de synchro réelle, ou est-ce que je me prouve juste que je sais le faire ? The summit of the type language: composing types from strings — `on${Capitalize<K>}` —, multiplying unions, parsing with infer. Vol 6's synthesis, and its brake. The power is such that you encode the unreadable without noticing. The only question: does this computed type buy me a real sync guarantee, or am I just proving I can do it?
Vol. VII La frontière typé / non-typéThe typed / untyped boundary
Où la donnée devient-elle typée ?Where does data become typed? · 3 numérosissuesou 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.
don't validate,
ou transformer l'inconnu en prouvé Parse,
don't validate,
or turning unknown into proven
Valider — vérifier un booléen puis caster en as — rouvre le trou : le check et le type dérivent, le as est un pari. Parser fait l'inverse, en un geste : une fonction prend de l'unknown et rend une valeur typée prouvée, ou échoue. Le schéma est la source unique — il valide, et z.infer en dérive le type. On parse une fois, au bord ; le cœur fait confiance. Le réflexe le plus quotidien de la série. Validating — checking a boolean then casting with as — reopens the hole: check and type drift, the as is a bet. Parsing does the opposite, in one move: a function takes unknown and returns a proven typed value, or fails. The schema is the single source — it validates, and z.infer derives the type. Parse once, at the edge; the core trusts. The series' most everyday reflex.
ou vérifier sans élargir satisfies,
or check without widening
Pour une valeur qu'on écrit : l'annotation vérifie mais élargit (on perd les clés exactes), le as force et peut mentir. satisfies fait les deux bons gestes — il vérifie la conformité ET garde le type précis inféré. Là où tu mettais un as pour « faire respecter » une forme, tu voulais satisfies : contrôler, pas forcer. Sa portée : les valeurs que tu rédiges, pas la donnée du dehors (ça, c'est parse). For a value you write: the annotation checks but widens (you lose the exact keys), as forces and can lie. satisfies does both right things — it checks conformance AND keeps the precise inferred type. Where you put an as to 'enforce' a shape, you meant satisfies: check, don't force. Its scope: values you author, not outside data (that's parse).
Vol. VIII L'asynchrone typéTyped async
Que garantit ce `Promise<T>` ?What does this `Promise<T>` guarantee? · 3 numérosissuesou la moitié de l'histoire Promise<T> & async/await,
or half the story
async/await a l'air de rendre l'asynchrone sûr — et pour le succès, c'est vrai : Promise<T> nomme exactement ce qu'on obtient si ça résout, async enveloppe toujours, les promesses s'aplatissent. Mais le type ne raconte que la moitié de l'histoire : le T est la valeur de succès, et la branche du rejet est totalement absente. Réflexe : demander à chaque Promise<T> ce qu'elle garantit — et ce qu'elle tait. async/await looks like it makes asynchrony safe — and for success, it does: Promise<T> names exactly what you get if it resolves, async always wraps, promises flatten. But the type tells only half the story: T is the success value, and the reject branch is entirely absent. Reflex: ask of every Promise<T> what it guarantees — and what it withholds.
ou pourquoi catch (e) est unknown The untyped error,
or why catch (e) is unknown
TypeScript ne type pas les rejets : un throw accepte n'importe quelle valeur, donc catch (e) est unknown, jamais MyError. La fausse symétrie avec Result<T, E> tombe — Promise<T> n'a pas de canal d'erreur. Deux issues honnêtes : rétrécir le unknown (instanceof), ou modéliser l'échec attendu comme une valeur typée (union discriminée). Le catch (e: any) qui lit e.message plante sur le premier rejet bizarre. TypeScript doesn't type rejections: a throw accepts any value, so catch (e) is unknown, never MyError. The false symmetry with Result<T, E> falls — Promise<T> has no error channel. Two honest ways: narrow the unknown (instanceof), or model the expected failure as a typed value (discriminated union). The catch (e: any) reading e.message crashes on the first odd reject.
ou le type dit comment ça échoue Combining promises,
or the type says how it fails
Promise.all lance en parallèle et rend un tuple typé — mais rejette en bloc au premier échec. allSettled ne rejette jamais et rend une union discriminée par résultat (le Vol 3 revient). race et any rendent une union, réglée au premier. Deux pièges : le tuple de all se perd si le tableau n'est pas inline, et all sur des opérations indépendantes sacrifie le succès partiel. Promise.all runs in parallel and returns a typed tuple — but rejects wholesale on the first failure. allSettled never rejects and returns a discriminated union per result (Vol 3 returns). race and any return a union, settled on the first. Two pitfalls: all's tuple is lost if the array isn't inline, and all on independent operations sacrifices partial success.
Vol. IX Le réglage du compilateur & le coûtCompiler config & cost
Réglé pour prouver, ou laisser passer ?Tuned to prove, or to let through? · 3 numérosissuesou chaque drapeau est un théorème strict & its flags,
or each flag is a theorem
Régler le compilateur est le terrain du cargo cult : on copie un tsconfig sans savoir ce qu'il prouve. Or chaque drapeau strict active un théorème précis pour un prix précis — strictNullChecks rend l'absence explicite, noImplicitAny ferme le trou des entrées. Et strict n'active pas tout : noUncheckedIndexedAccess (arr[i] : T | undefined) reste dehors. On règle cran par cran, en comprenant — jamais en copiant, jamais tout d'un coup sur du legacy. Tuning the compiler is cargo-cult terrain: you copy a tsconfig without knowing what it proves. Yet each strict flag turns on a precise theorem for a precise price — strictNullChecks makes absence explicit, noImplicitAny shuts the input hole. And strict doesn't enable everything: noUncheckedIndexedAccess (arr[i] : T | undefined) stays outside. You tune notch by notch, understanding — never copying, never all at once on legacy.
ou gratuit au runtime n'est pas gratuit The compile cost,
or free at runtime isn't free
Les types s'effacent à l'exécution — donc on les croit gratuits. À la compilation, le vérificateur les exécute : récursions déroulées, produits cartésiens matérialisés, structures comparées. Un type trop malin ne ralentit pas ton programme, il ralentit tsc et l'éditeur, à chaque frappe. On mesure avec --extendedDiagnostics ; et au-delà d'une limite, tsc casse, ou cesse de comparer et suppose les types compatibles, en silence. Souvent, un type explicite bat un type malin et lent. Types erase at runtime — so you think them free. At compile time, the checker runs them: unrolled recursions, materialized cartesian products, compared structures. A too-clever type doesn't slow your program, it slows tsc and the editor, on every keystroke. You measure with --extendedDiagnostics; and past a limit, tsc breaks, or stops comparing and silently assumes the types compatible. Often, an explicit type beats a clever, slow one.
ou où la preuve s'arrête The limits & external lies,
or where the proof stops
Tout l'édifice de types repose sur des déclarations qu'on n'a pas écrites ni vérifiées : .d.ts fournis, @types tiers, declare maison. Un .d.ts peut affirmer n'importe quoi sur du JavaScript qui fait autre chose ; skipLibCheck ne les vérifie même pas. Ta garantie n'est jamais plus forte que la déclaration la plus faible sur le chemin. Aux frontières des libs mal typées, on revient au Vol 7 : on valide, on ne croit pas. Fin de la série. The whole type edifice rests on declarations you didn't write or verify: bundled .d.ts, third-party @types, homemade declare. A .d.ts can assert anything about JavaScript that does otherwise; skipLibCheck doesn't even check them. Your guarantee is never stronger than the weakest declaration on the path. At mistyped libs' boundaries, you return to Vol 7: validate, don't trust. End of the series.
Pas une syntaxe. Un modèle. Not a syntax. A model.
Le piège, en apprenant TypeScript, c'est de faire taire le compilateur — de mémoriser les incantations (as, any, !) qui « font passer les types » sans comprendre ce qu'elles prouvent, ni ce qu'elles cessent de prouver. Chaque numéro dissèque une seule tension : forme, effacement, type, frontière. Le bug qu'on n'aura pas d'abord, le prix ensuite, et le contre-exemple — quand ne pas l'employer. The trap, learning TypeScript, is silencing the compiler — memorizing the incantations (as, any, !) that 'make the types pass' without grasping what they prove, or what they stop proving. Each issue dissects a single tension: shape, erasure, type, boundary. The bug you won't have first, the price next, and the counter-example — when not to use it.
La règle tacite : en TypeScript, le type prouve à la compilation puis s'efface à l'exécution. La discipline tient en deux gestes — pousser les invariants dans le type, et savoir exactement où la preuve s'arrête : la frontière avec les données non typées. Chaque refus du compilateur est un théorème qu'on n'a pas encore su énoncer — ou un mensonge qu'on s'apprêtait à écrire. The unspoken rule: in TypeScript, the type proves at compile time, then vanishes at runtime. The discipline is two moves — push invariants into the type, and know exactly where the proof stops: the boundary with untyped data. Every refusal is a theorem you haven't yet learned to state — or a lie you were about to write.