---
title: "25 — Type Checkers — Catching the Bug Before the Code Runs"
uid: type-checkers
tags: ["type-checking", "tooling", "typescript", "static-analysis", "types", "roadmap:frontend"]
excerpt: "A type checker is a compile-time proof-reader — it turns runtime crashes into editor red squiggles, catching whole classes of bugs before the code ever runs."
date: 2026-08-12T18:35:08+0000
source: https://www.aveshina.my.id/en/blog/type-checkers
---

"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:

```figure
<svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Two timelines for the same bug. Top timeline, labelled WITHOUT a type checker: write code, then run, then the program crashes at runtime with TypeError. Bottom timeline, labelled WITH a type checker: write code, the type checker stops the bug in the editor with a red squiggle before run, and run never sees the bug. The bug is the same; only when it is caught differs.">
  <defs>
    <marker id="tcarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- Top row: no type checker -->
    <text x="20" y="48" font-size="12" font-weight="700" fill="#500724">WITHOUT a type checker</text>
    <rect x="20" y="62" width="130" height="40" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="85" y="86" font-size="11" fill="#052e16" text-anchor="middle">write code</text>
    <line x1="150" y1="82" x2="190" y2="82" stroke="#64748b" stroke-width="1.5" marker-end="url(#tcarrow)"/>
    <rect x="190" y="62" width="130" height="40" rx="6" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="255" y="86" font-size="11" fill="#422006" text-anchor="middle">run</text>
    <line x1="320" y1="82" x2="360" y2="82" stroke="#64748b" stroke-width="1.5" marker-end="url(#tcarrow)"/>
    <rect x="360" y="62" width="340" height="40" rx="6" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="530" y="80" font-size="11" font-weight="700" fill="#500724" text-anchor="middle">runtime crash — TypeError</text>
    <text x="530" y="94" font-size="9.5" fill="#500724" text-anchor="middle">bug caught in production, by a user</text>

    <!-- Divider -->
    <line x1="20" y1="135" x2="700" y2="135" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="4 4"/>

    <!-- Bottom row: type checker -->
    <text x="20" y="168" font-size="12" font-weight="700" fill="#1e1b4b">WITH a type checker</text>
    <rect x="20" y="182" width="130" height="40" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="85" y="206" font-size="11" fill="#052e16" text-anchor="middle">write code</text>
    <line x1="150" y1="202" x2="190" y2="202" stroke="#64748b" stroke-width="1.5" marker-end="url(#tcarrow)"/>
    <rect x="190" y="182" width="180" height="40" rx="6" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="280" y="200" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">type checker</text>
    <text x="280" y="214" font-size="9.5" fill="#475569" text-anchor="middle">red squiggle in the editor</text>
    <line x1="370" y1="202" x2="410" y2="202" stroke="#db2777" stroke-width="1.5"/>
    <text x="390" y="196" font-size="14" font-weight="700" fill="#db2777" text-anchor="middle">✕</text>
    <rect x="410" y="182" width="130" height="40" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5" stroke-dasharray="4 4"/>
    <text x="475" y="206" font-size="11" fill="#64748b" text-anchor="middle">run — bug-free</text>
    <text x="20" y="252" font-size="10.5" font-style="italic" fill="#64748b">same bug — the only difference is *when* it gets caught</text>
  </g>
</svg>
```

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](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](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](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](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](https://www.typescriptlang.org/docs/handbook/type-compatibility.html)

```quiz
Q: TypeScript types change what the code does at runtime. True or false?
- True — the runtime sees the types
- False — types are erased at compile time; the browser runs plain JavaScript
correct: 1
explain: TypeScript adds a compile-time type layer that is stripped away by the compiler. The runtime output is plain JavaScript with no type information, so types can only constrain what you write, not change what the code does.

Q: TypeScript uses structural typing. That means type compatibility is decided by…
- the type's declared name (nominal identity)
- the type's shape — whether it has the required fields
correct: 1
explain: TS compares shapes, not names. Two types with the same fields are compatible even with no declared relationship. This is duck typing checked at compile time, the opposite of nominal typing in Java or C#.

Q: You write `const count = 0`. The type checker treats `count` as…
- untyped — JavaScript is dynamic so it has no type
- `number`, inferred from the initial value
correct: 1
explain: TS infers the type from how the value is produced. `0` is a number, so `count` is `number` without any annotation needed. Inference is why the annotation cost is far lower than it appears.

Q: A function correctly returns the right *shape* of value but the wrong *actual* value. What does the type checker do?
- It catches it — types prevent logic errors
- It can't catch it — types guard shape, not logic
correct: 1
explain: Types narrow the space of possible bugs but don't empty it. A type checker verifies that operations are valid for the value's type; it has no opinion on whether the value itself is correct.

Q: Why does `any` defeat the purpose of a type checker?
- It doesn't — `any` is just shorthand for `string`
- It's an escape hatch that makes the value invisible to the checker, opening a hole it can't see through
correct: 1
explain: `any` opts out of type checking entirely — the checker treats an `any`-typed value as compatible with anything, so no errors are reported for it. The discipline is to use proper types or the type-safe `unknown` instead.
```
