Types don’t just describe your code. They test it too.

Types are a superpower.

They document your code, test your code, and unlock powerful tooling. In languages like Rust, the type system isn’t just a nice-to-have - it’s a core part of what enables both high performance and high reliability. Used well, your type checker becomes your first line of defense, catching entire classes of bugs before you even run your code.

Types eliminate entire families of tests

Let’s say I have a function that returns a String. Thanks to the type system, I know with absolute certainty that it will only return a String. Not an i64. Not null. Not undefined. Just a String.

That means I don’t have to write tests to check for unexpected types or null values - the compiler has already guaranteed that for me.

Even better, if I later change that return type to Option<String>, the compiler will instantly flag every usage of that function where I forgot to handle the None case. It’s like having a tireless assistant showing you exactly where things need to change. A type checker just saved you from hours of debugging and subtle bugs.

Types ensure the right data gets in

The same applies to inputs.

In dynamically typed languages, you might start a function with a cascade of if statements:

if (!email || typeof email !== "string") throw new Error("Invalid input")
if (!email.includes("@")) throw new Error("Not an email")

In Rust or TypeScript, you don’t need to do that. You can just say:

fn send_email(to: Email) { ... }

And boom - your function only gets called if someone passes in a properly constructed Email value. No ifs, no maybes. The type has done the heavy lifting for you.

Types help you DRY things out

Let’s stick with that Email type.

Somewhere in your codebase, you’ll need to convert from a raw string to an Email. In Rust, you might implement this via TryFrom<String> for your Email type. Then you test that one implementation:

impl TryFrom<String> for Email { ... }

Now, anywhere else in your code, you can assume that if a value is of type Email, it’s already been validated. You don’t need to re-test email validation in every single place you use it.

One test. One type. Confidence everywhere.

Types are mandatory, Tests are optional

Here’s another killer feature of types: you can’t skip them.

Tests are great - but they’re also voluntary. You can comment them out, forget to run them, or skip them in a hurry. The compiler? It doesn’t give you a choice. If your code violates type constraints, it simply won’t build.

This is especially valuable in teams. You can bring in new contributors - even junior developers - and feel confident they won’t accidentally break something fundamental. Types are always watching.

Tests still matter - especially for business logic

All that said, types don’t test everything.

They won’t tell you whether a discount was correctly applied to a shopping cart, or whether your sorting algorithm really works as expected. That’s where tests come in - particularly when verifying business logic.

But once you’ve tested that your Email or UserId type behaves correctly, you can skip testing that same logic everywhere else. Cover your types with tests, and the rest of the system benefits.

Conclusion

Types can drastically reduce the number of tests you need. But tests still matter.

  • Cover your types with tests.
  • Let your types protect your other code.
  • Understand that TDD cannot replace the compiler, and the compiler cannot replace TDD.

The same way tests don’t replace all your other tools - as I wrote before - types are another powerful tool in your toolbox.

As I’ve found, when used together, types and tests work wonders - giving you the best of both worlds: speed, safety, and confidence.


Types don’t just describe your code. They test it too.