The Benefits of Test-Driven Type-Driven Development

Type-Driven development is the practice of defining types first, in order to build out the implementation.

The Benefits of Type-Driven Development

My colleague Andy describes his experience using this pattern, and notes that it’s able to replace some unit tests. This has also matched my experience. Well-defined types surrounding certain functions and data types have largely rendered some unit tests unnecessary.

Overall, I’ve found that the type-driven development pattern works well, and compliments the test-driven development style that many of us at Atomic favor. For the past year (since the release of TypeScript 3.9), I’ve discovered a new working style: Test-Driven Type-Driven development.

Let me explain.

Testing Types

TypeScript 3.9 added the @ts-expect-error comment notation. This suppresses an error on the next line but also acts as validation by resulting in an error if there is none. Because this verifies type errors, we can use the @ts-expect-error to check that we’re building and using types correctly. Just as in traditional test-driven development, we can write our assertions before writing the implementation.

Here’s a scenario I ran into a little while ago. I wanted to build a helper function to accept all of the named colors from the Grommet Color type. The provided color type includes a list of named colors (e.g. background-back, dark-1, status-ok, etc.) and an index type [x: string]: ColorType; property. I wanted the helper function to only accept the named colors, to provide some validation, and only accept expected colors. With the index type included, any string would be considered “valid.” Therefore, I wanted a type to remove the additional index type rather than re-declaring a new type.

Test-Driven Development, But with Types!

Even before our writing our “test,” I want to verify that this will actually behave like I would expect:

verifying behavior works prior to writing the implementation

After we’ve verified that this approach will work, we need to set up the “test” for our type:

test setup

After we’ve set up the test, we can make it fail with the @ts-expect-error helper:

first failing test

Once we have a failing test, we can implement the type:

first passing test

After having an appropriate input and output types, we can then implement our functions, writing any necessary tests required for additional validation. In my experience, having proper types before writing (or refactoring) code improves the developer experience and lowers the chances of bugs.

Comibining Test-Driven Practices with a Type-Driven Pattern

As you can see, we can utilize test-driven practices even when practicing a type-driven development pattern. TypeScript offers a lot of flexibility when doing this, and I highly recommend this practice!