In dynamic TDD, half your tests are written to guard against nulls, missing keys, and invalid combinations. A modern type system eliminates whole classes of mutants at compile time, freeing TDD to focus 100% on domain behavior.
type CommentThread = {
id: number;
author: {
name: string;
isMod: boolean;
};
status: "published" | "draft" | null;
replies: CommentThread[]; // Recursive!
};
{
id: 42,
author: {
name: "Alex",
isMod: true
},
status: "published",
replies: [ /* recursive children */ ]
}
Key insight: The type tree defines the possibility space (the geometry). A runtime object is just one specific harvest hanging from the tree's valid branches.
In mutation testing (Stryker, Pitest), a tool systematically mutates your code to see if your test suite catches it. In a dynamic language, dozens of mutants survive unless you write tedious "defensive shape tests". With strict types, the compiler kills entire categories of mutants statically at 0ms cost.
A unit test proves an existential claim (∃):
test("returns 42 for input 'Alex'") => ∃ x : f(x) = 42
It verifies finite discrete sample points.
A type system proves a universal claim (∀):
∀ x ∈ string, f(x) ∈ number
It mathematically guarantees that across infinite possible inputs, the shape invariant can never be breached.
Product Type (Record): $|A \times B| = |A| \cdot |B|$
If $A$ has 3 states and $B$ has 4 states, the product space is $3 \times 4 = 12$ combinations that must all exist simultaneously.
Sum Type (Union): $|A + B| = |A| + |B|$
A union has only $3 + 4 = 7$ states. It collapses combinatorial state explosion, making invalid combinations physically impossible to construct.
Types do not make TDD obsolete; types liberate TDD from testing the plumbing. When types guarantee presence, nullability, structure, and state validity, 100% of Alex's unit tests and mutation tests can be dedicated to what actually matters: business domain invariants and calculation logic.