Tags: web-dev concept

Type Systems

Date: 2026-08-17


Rules about what kinds of value can appear where. “Static versus dynamic” and “strong versus weak” are four independent axes routinely collapsed into one — and keeping them apart is what makes the arguments about them tractable.


A type system constrains which operations are valid on which values. The differences between languages sit on several separate axes, and conflating them is why the debate is so muddled.

Four axes, not one

STATIC ←────────────→ DYNAMIC
when are types checked?
compile time          run time
TypeScript, Java      JavaScript, Python

STRONG ←────────────→ WEAK
how much implicit coercion?
Python: "1" + 1 errors
JS:     "1" + 1 → "11"

EXPLICIT ←──────────→ INFERRED
must you write the type?
Java (mostly)         TypeScript, Rust

NOMINAL ←───────────→ STRUCTURAL
what makes two types the same?
the NAME               the SHAPE
Java, C#               TypeScript, Go

Python is dynamic and strong. JavaScript is dynamic and weak. Those are different claims, and treating “dynamic” as meaning “weak” is the most common confusion here.

Nominal versus structural

The axis that surprises people arriving at TypeScript from Java or C#:

NOMINAL — the name decides
  class Metres { value: number }
  class Feet   { value: number }
  → different types. Cannot mix.

STRUCTURAL — the shape decides
  type Metres = { value: number }
  type Feet   = { value: number }
  → THE SAME TYPE. Freely mixed.

TypeScript is structural (Structural Typing), so those two are interchangeable and the type system will not stop you adding feet to metres. The workaround is branding — adding a phantom property that makes the shapes differ:

type Metres = number & { __unit: 'm' }
type Feet   = number & { __unit: 'ft' }

Ugly, and the only way to get nominal behaviour in a structural system.

What a static type system actually buys

  • A class of bug becomes impossible rather than merely tested for — typos in property names, wrong argument order, unhandled null
  • Refactoring becomes mechanical. Rename a field and the compiler lists every site. This is the largest practical benefit and the one hardest to appreciate before experiencing it
  • Types are documentation that can’t drift, because they’re checked
  • Editor tooling — autocomplete and inline errors follow from types existing

What it costs

  • Ceremony, especially at boundaries where data is genuinely unknown
  • A learning curve for the type language itself, which in TypeScript’s case is substantial and Turing-complete
  • False confidence. This is the one that matters:
const data: User = await res.json()
                   ↑ this is a LIE
 
json() returns any. TypeScript has been
told it's a User and will check nothing
at runtime.

Types describe your beliefs about data, not the data. At every external boundary — an API response, localStorage, a URL parameter, a form — the type is an assertion, not a verification. Validate at the boundary with a runtime schema validator, then let types carry it inwards from there — Type Safety Boundaries.

Making illegal states unrepresentable

The technique that turns a type system from documentation into a design tool:

// permits nonsense
type State = {
  loading: boolean
  data?: Data
  error?: Error
}   // loading AND error AND data — legal
 
// permits only sense
type State =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'ready';  data: Data }
  | { status: 'error';  error: Error }

The second is a discriminated union, and it removes the defensive checks entirely — data only exists where it’s meaningful, and the compiler enforces that you handled every case — State Machines.

Gradual typing

TypeScript, Python’s hints and PHP’s declarations are all gradual — types are optional and coexist with untyped code.

The practical consequence is that any is contagious. One any disables checking for everything downstream of it, which is how a codebase ends up nominally typed and actually unchecked. unknown is the honest alternative: it forces a narrowing check before use — Type Narrowing.

Strictness is a dial, not a switch, and the useful move on an existing codebase is to turn it up incrementally rather than to argue about whether to adopt types at all.