open atlas
↑ Back to track
TypeScript type system, deep TS · 00 · 01

Why types, deeply

TypeScript types are a compile-time proof about the shape of runtime values that erases entirely before the code runs — a type error is a counterexample the checker found before your users did, not a runtime guard.

TS Junior ◷ 12 min
Level
FoundationsJuniorMiddleSenior
Already know this unit? Take a 1-minute quick check →

You rename a function: getUser(id: string) becomes getUser(id: string, opts: { fresh: boolean }). There are 23 call sites across the codebase. In a plain-JavaScript world you grep, you fix what you find, you ship — and three weeks later a forgotten call site in an error path throws Cannot read properties of undefined. In TypeScript, the moment you change the signature, the compiler paints all 23 stale call sites red before you ever run the program. That is not autocomplete being helpful. That is a machine proving, statically, that your change is incomplete.

By the end of this lesson you will know exactly why that proof works, why it vanishes at runtime, and where the gaps are — so you stop treating a green compile as a guarantee.

Types are a proof, not a hint

The shallow pitch for TypeScript is “autocomplete and red squiggles.” That undersells it. A type annotation is a claim about every value that will ever flow through a position at runtime, and the type checker is a proof engine that either confirms the claim holds on every path or hands you a concrete counterexample — the exact line where a value of the wrong shape can arrive.

function totalCents(items: { priceCents: number }[]): number {
  return items.reduce((sum, it) => sum + it.priceCents, 0);
}

totalCents([{ priceCents: 100 }, { priceCents: 250 }]);
// returns 350

totalCents([{ price: 100 }]);
// Error: Object literal may only specify known properties, and
// 'price' does not exist in type '{ priceCents: number; }'.

The second call never runs. The checker rejected it because the value’s shape contradicts the function’s contract. You did not write a test for “what if someone passes price instead of priceCents” — the type system covered that case, and every other shape mismatch, exhaustively.

The refactor payoff: every stale call site lights up

This is where types earn their keep on a real codebase. Change a contract and the checker enumerates every place that no longer satisfies it.

// before
function track(event: string) { /* ... */ }
track("checkout");

// after: you make the second arg required
function track(event: string, props: Record<string, unknown>) { /* ... */ }

track("checkout");
// Error: Expected 2 arguments, but got 1.
//        An argument for 'props' was not provided.

In JavaScript this is a silent landmine — props is undefined, and the bug surfaces only on the code path that reads it, possibly in production. In TypeScript the incomplete refactor is a compile error at every missed call site simultaneously. The checker is doing a whole-program search you would otherwise do by hand and grep.

Types erase: they are gone before the code runs

Here is the fact that reframes everything. TypeScript types have zero runtime representation. The compiler checks them, then strips them out. The JavaScript that actually executes has no idea types ever existed.

// input.ts
function greet(name: string): string {
  return `Hello, ${name}`;
}
const x: number = 41 + 1;
// emitted output.js — annotations are gone
function greet(name) {
  return `Hello, ${name}`;
}
const x = 41 + 1;

The : string, the : number, the return annotation — all erased. This is type erasure, and it has a hard consequence: types cannot validate runtime data. A type only constrains values the compiler can see at build time. The instant a value crosses a boundary the compiler cannot see — a fetch() response, JSON.parse, a form body, process.env — the type is an assumption, not a guarantee.

const res = await fetch("/api/user");
const user: { id: string; name: string } = await res.json();
//    ^? const user: { id: string; name: string }
user.name.toUpperCase();
// Compiles fine. But res.json() is typed `any` (well, `Promise<any>`),
// so this annotation is a PROMISE YOU made to the compiler, not a fact.
// If the server returns { id: 7, name: null }, this throws at runtime.

The annotation says “trust me, this is the shape.” The compiler believes you. If the wire data disagrees, the type bought you nothing. Real systems close this gap with a runtime validator at the boundary — and that is exactly what lesson 08’s zod material is about: re-establishing, at runtime, the contract the type system assumes statically.

Why this works

Why can the compiler erase types and still be useful? Because soundness here is a build-time property over a closed program. Inside the code TypeScript can see, every assignment, return, and call is checked against the declared types, so within that closed world the proof holds. Erasure is safe precisely because the proof was already discharged at compile time — the runtime does not need the types to be correct; it only needed them while the checker was running. The catch is that the closed world ends at every I/O boundary, where un-checked any-typed data leaks in.

The honest cost: any, structural escape hatches, and unsoundness

A green compile is not a correctness proof. TypeScript is deliberately gradual and unsound in specific, documented places, so you can adopt it incrementally and model real JavaScript.

const data: any = JSON.parse(localStorage.getItem("cart")!);
data.foo.bar.baz();          // no error — `any` disables all checking
data.totalCents.toFixed(2);  // no error — and may explode at runtime

any is a hole in the proof. Any value flowing through an any is exempt from checking and can re-enter typed code as a lie. Other documented escape hatches that produce a green compile over code that can still crash:

const el = document.querySelector(".btn") as HTMLButtonElement;
//                                        ^ assertion: you override the checker
el.disabled = true;          // throws if the element wasn't found (el is actually null)

const arr: number[] = [1, 2, 3];
const n = arr[10];
//    ^? number   <-- LIES. n is `undefined` at runtime (unless noUncheckedIndexedAccess)

These are not bugs in TypeScript; they are the price of a type system pragmatic enough to wrap a 30-year-old dynamic language. When you see any, as, or an unguarded index access in a code review, treat it as a place where the proof has a deliberate gap — and budget a runtime check there accordingly.

Quiz

Your function annotates a fetch response as `{ id: string }`, it compiles clean, but in production `id` is sometimes a number and code crashes. What actually happened?

Order the steps

Order what happens to a typed value, from authoring to a possible runtime failure on bad I/O data.

  1. 1 You write a value plus a type annotation in .ts source
  2. 2 tsc checks the value against the declared type at build time
  3. 3 tsc erases the annotations and emits plain .js
  4. 4 An unchecked fetch/parse value enters through an I/O boundary at runtime and can violate the erased contract
Recall before you leave
  1. 01
    Why is 'types are just autocomplete' a wrong mental model, and what is the accurate one?
  2. 02
    What is type erasure and what is its single most important consequence?
  3. 03
    Why is a clean compile not a proof of correctness?
Recap

The durable model: TypeScript types are a static proof about the shape of runtime values, discharged by the checker at build time and then erased — the emitted JavaScript carries no type information at all. That is why types are superb at catching incomplete refactors (every stale call site lights up at once) yet powerless against unchecked I/O data, and why a green compile is not a correctness proof while any, assertions, and index access remain open holes. Now when you see a fetch response annotated with a specific type and the function compiles clean, you will ask the right question first: “Is there a runtime validator at this boundary, or is this annotation just a promise we made to the compiler?”

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.