runtimes · level 2

Static vs Dynamic Typing

When type errors surface — and why TypeScript sits in the middle.

200 XP

Static vs Dynamic Typing

A type system is a classifier: it assigns each expression a type and refuses programs that mix types in nonsensical ways. The important question for this level is when that check happens.

Analogy

Think of two ways to catch baggage that doesn't fit on a plane. Static is the oversize-bag frame at the check-in desk: if the suitcase doesn't slide through, the passenger never reaches the gate — annoying at the counter, but the plane always takes off cleanly. Dynamic is letting everyone board and only checking bags when the overhead bin won't close — faster boarding, but when it fails, it fails mid-flight with people watching. Same physical bags, same physical bins; only the moment of the check differs.

Static typing

Statically typed languages — Rust, Go, Java, Kotlin, Swift, TypeScript — run the type checker at compile time. The compiler walks the AST, infers or reads declared types, and refuses to emit an executable if anything is inconsistent.

let n: u32 = "hello";
// rustc: expected `u32`, found `&str`. No binary is produced.

What you gain: a large class of bugs (null derefs, wrong argument types, missing enum arms) never reach production. Refactoring is much safer because the compiler tells you every call site you need to update.

Dynamic typing

Dynamically typed languages — Python, Ruby, classical JavaScript, Lua — don't do type-checking up front. Types live on values, not on variables, and mismatches are discovered only when that line of code executes.

def greet(u):
    return "Hello, " + u.name

greet(None)   # AttributeError at runtime

What you gain: extreme flexibility. Monkey-patching, runtime introspection, duck typing, and rapid prototyping all come cheap. What you lose: any line that's never exercised never gets checked — which is why dynamically-typed projects rely heavily on tests for what static types would catch for free.

Gradual typing

The modern compromise. TypeScript bolts a static type system onto JavaScript: you write annotations, the tsc compiler checks them, then strips them out to produce plain JS. At runtime, TypeScript is JavaScript — the checks exist only at build time.

function add(a: number, b: number): number {
  return a + b;
}
add(1, "2"); // tsc errors; the emitted JS would happily coerce.

Python 3.5+ added type hints (PEP 484). Like TypeScript, they're not enforced by the interpreter; you run mypy or pyright as a separate step. Python doesn't strip them — they're introspectable via __annotations__ and used heavily by frameworks like FastAPI and Pydantic for validation.

The point of gradual typing: you can migrate a codebase from untyped to typed file by file without a rewrite.

Structural vs nominal

Two different rules for "when are two types equal?"

Nominal: types are equal if they have the same name. Java, C#, Rust, Swift.

class Cat { void meow() {} }
class Dog { void meow() {} }
Cat c = new Dog(); // error — even though methods match

Structural: types are equal if they have the same shape. TypeScript, Go (interfaces), OCaml (objects).

type Meower = { meow(): void };
class Dog { meow() {} }
const m: Meower = new Dog(); // ok — shape matches

Go's interfaces are a famous example: you don't declare implements io.Reader anywhere — if your type has a matching Read([]byte) (int, error) method, it satisfies the interface automatically. That's structural typing.

Soundness vs expressiveness

TypeScript is deliberately unsound — it allows some provably-unsafe programs because the alternative was too restrictive for JavaScript idioms. any is the escape hatch. Rust's type system is sound for ownership (no use-after-free without unsafe) but refuses many correct programs the borrow checker can't prove safe. Every type system picks a point on that Pareto curve.

What it means operationally

In an AOT + statically-typed stack (Rust, Go, Kotlin), the compiler refuses to build. You never ship the bug. In a JIT + dynamic stack (Node.js, PyPy), the bug runs in production and shows up in your error tracker. Your test coverage has to be dense enough to compensate — or you adopt TypeScript / mypy and drag the checks back toward build time.