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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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?
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.
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.
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).
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.
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.
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.
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.
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 dégrade en silence vers any. 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 silently degrades to any. Often, an explicit type beats a clever, slow one.
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.
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.