There’s a recurring debate about whether you need tests if you have types, or vice versa. The framing is wrong. They’re not interchangeable — but if I had to pick one, I’d pick types every time.

The coverage illusion

A test checks one example. Even property-based tests check a sample. A type annotation checks every possible value that will ever flow through that code path. When you write fn process(input: ValidatedOrder), you’ve made it structurally impossible to call that function with unvalidated data. No test gives you that.

Tests tell you “this specific scenario works.” Types tell you “this category of mistake cannot happen.”

Where tests still win

Types can’t verify business logic. They can’t tell you that your pricing calculation produces correct results, or that your state machine transitions are valid. That’s where tests earn their keep.

The sweet spot: use types to make illegal states unrepresentable, then test the logic that operates within those constraints. You end up writing fewer tests, and each one is more meaningful.

The practical upshot

If you’re starting a project and choosing where to invest first — invest in your type definitions. Model your domain with types that rule out invalid states. Then write tests for the behavior that remains.

You’ll ship fewer bugs, and you’ll write less test code to get there.