25 — Type Checkers — Catching the Bug Before the Code Runs
"Extra annotation noise that slows me down" was my type-checker dismissal, and it held until the first runtime crash a squiggle would have caught. The line that everything else hangs off: *a type checker is a static proof-reader that catches a whole class of bugs before the code runs, not after.* "This value is a string, not a number" is a crash at runtime (while the app is actually running) in JavaScript. The exact same mistake, with a type checker, is a red squiggle in the editor — caught, fixed, and never shipped [1].
The way of thinking that made it click: TypeScript adds a compile-time type layer (a check that happens at build/packaging time, before the code ever runs) on top of JavaScript, and that layer is erased at runtime [2][3]. It doesn't change what the code does. It constrains what I'm allowed to write. The runtime output is plain JavaScript — no types survive into the browser. So the type checker isn't a new language that runs differently; it's a guardrail that vetoes mistakes at authoring time and then steps out of the way.
The split I had to see clearly before any of it made sense:
Once I had that picture, the "why bother" question answered itself: the bug is the same in both timelines. The type checker doesn't make me a better programmer who writes fewer bugs — it moves the discovery of a particular family of bugs left, from a user's screen in production to my editor before I save.
Why types at all
The thing I had to straighten out first is what a type even is. A type is a label on a value that says "this is a string," "this is a number," "this is a User object." Operations are defined per type — you can .toUpperCase() a string, but the operation is meaningless on a number. A type checker's job is to verify, for every operation in the program, that the value's type actually supports it [1][4].
Three concrete reasons I now reach for types on anything non-trivial:
- Catch errors before runtime. The whole pitch. A function that expects a User and is handed an Order is caught at the call site, in the editor, not in a stack trace (the error log) six requests deep. This is the difference between static checking (before the code runs) and dynamic checking (during the run) — JavaScript does the latter, TypeScript adds the former [1].
- Self-documenting signatures. A function signature getUser(id: string): User tells me what goes in and what comes out without reading the body. In plain JavaScript the same function is just getUser(id) and I have to grep the source to learn the shape of id and the return value. Types are a contract the IDE (code editor) can render as a tooltip.
- Refactoring safety. Rename a field on a type and every place that reads the old field lights up red. Change a function's return shape and every consumer breaks at compile time. Without types, refactoring is a faith exercise — I cross my fingers and run the test suite, hoping the parts the tests don't cover still work.
TypeScript specifically
JavaScript is dynamically typed — the type of a value is checked at the moment an operation runs, and a variable can hold a string now and a number later. That flexibility is why a single typo or a missing API field can sail through "it works on my machine" and crash only in production. TypeScript overlays a static type system on JavaScript, and it's the type checker that has effectively become the default for the JS ecosystem [2][3].
Three properties that make TS work the way it does, and that I had to learn separately because each trips up newcomers:
- Structural typing. TS doesn't care about a type's name — it cares about its shape. If User has { id: string; name: string } and some other object happens to have those same fields, TS treats them as compatible even with no declared relationship. This is "duck typing checked at compile time" rather than runtime. It's the opposite of nominal typing (Java, C#), where two types with identical fields but different names are unrelated [5].
- Inference. I rarely annotate every variable. const count = 0 is inferred as number; const name = "Ave" as string. The type checker works out the types from how values are produced and used, and I only add annotations where inference can't pin it down (function parameters, complex returns, intent I want to lock in). The cost of annotation is far lower than it looks [3].
- Erasure. Types exist only at compile time. The TS compiler strips every type annotation and emits plain JavaScript. At runtime, id: string is gone — the browser sees only id. This is why types can never change what the code does; they can only veto what I'm allowed to write. There is zero runtime cost from the type system itself [2][3].
The tradeoff
I'd be lying if I said types were free. The honest accounting:
- Annotation and learning cost. There's a syntax to learn (interface, type, generics — types that take other types as parameters — and unions — a value that can be one of several types) and a tax of writing signatures. For a one-off script this is pure overhead. The payoff scales with code size and team size — the larger and longer-lived the codebase, the more the type checker earns its keep.
- It's a guardrail, not a proof. TS catches type errors, not logic errors. A function typed to return the right shape can still return the wrong value. Types narrow the space of possible bugs; they don't empty it.
- any is an escape hatch that defeats the point. Every any in the codebase is a hole the type checker can't see through. The discipline is reaching for proper types and unknown (a type-safe counterpart that forces narrowing) instead of any when the shape is genuinely uncertain.
The practical line I settled on: types on anything I'll touch again, anything another person will read, or anything that talks to an API. Throwaway scripts stay in plain JavaScript. The crossover where the safety pays for the annotation cost is lower than I used to think.
How I use this
The habit these notes left me with is treating the type checker as a second pair of eyes that reads the code before it runs. When a red squiggle appears, my first instinct used to be annoyance — now I read it as the checker telling me about a bug I would otherwise have met in a stack trace. The type annotations are not ceremony; they're a way of stating intent that the machine can then enforce. Most of the value isn't in catching the exotic bugs. It's in catching the boring, obvious, would-have-cost-an-hour bugs — the wrong field name, the missing null check, the function called with the wrong argument order — at the moment I write them, instead of three weeks later.
References
[1] Finchtrade, "Type checking," Finchtrade Glossary, 2024. [Online]. Available: https://finchtrade.com/glossary/type-checking
[2] Microsoft, "TypeScript for the New Programmer," TypeScript Documentation, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html
[3] Microsoft, "The TypeScript Handbook," TypeScript Documentation, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/intro.html
[4] Microsoft, "TypeScript for JS Programmers," TypeScript Documentation, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html
[5] Microsoft, "Type Compatibility," TypeScript Documentation, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/type-compatibility.html
Knowledge check · Question 1 of 5
TypeScript types change what the code does at runtime. True or false?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!