Contents

Contents

Test-Driven Development and Type Systems Are Not Mutually Exclusive

Contents

There’s a common idea floating around (especially in FP-heavy circles and anti-TDDers) that if your language has a strong type system (Hindley–Milner, dependent types, etc.), you don’t need TDD anymore. That once we can encode business rules in types, the compiler “proves” our program is correct.

That’s a misunderstanding that I come across so many times.

Let’s clear this up: type systems, compilers, and TDD are not alternatives. They’re complementary tools. Each addresses a different aspect of correctness and confidence in our code.

A powerful static type system lets us express invariants and constraints in the structure of our program. We can rule out invalid states, prevent certain categories of bugs, and make many assumptions explicit.

This is incredibly valuable.

Languages like Haskell, F#, or OCaml excel at this, and make it easier to encode domain rules directly into types. This eliminates a lot of runtime errors that more dynamic or weakly typed languages would catch only through testing.

But even with the best type system on Earth, we still need to know: does the system actually do what the user or business wants?

TDD helps us define expected behavior before we implement it. We start with examples (concrete use cases) and build toward the code that makes them pass.

That feedback loop encourages modular, testable design, it validates assumptions as early as possible, and keeps our implementation aligned with real requirements.

Type systems let us say: “These states are impossible” TDD lets us say: “This behaviour should happen.” Property-Based Testing (PBT) lets us say: “This property must always hold” (test broad, invariant-based expectations).

They are all complementary, not mutually exclusive.

A function might be type-safe and still return the wrong result. A system might compile cleanly and still violate business rules. A property might hold in 1000 random cases but fail on the one that matters to your user.

In real-world systems, we want both structural safety and behavioral confidence.

Relying only on the compiler for correctness is not very different from the fallacious “compile therefore runs”. It’s a good start, but it’s not the whole picture.

So let’s stop framing strong types and TDD as rivals. They’re teammates, and the systems I trust most are the ones where both are used with care and intent.

Originally posted on LinkedIn.