Move fast — let the type system prevent you from breaking things!
As part of the Developer Experience team at DAZN, most of the time (99%) we work on internal products for our fellow engineers (our customers!).
Sometimes our products bring a lot of value but might have a very short lifecycle. Or at least this is what we know when we start developing them and we iterate fast to shorten the feedback loop.
There is a very thin line between over engineering a solution that you build in this context and creating something that is hard to maintain.
As ancient wisdom tells: there is no rewrite between moving from MVP to the production-ready application.
Recently in projects like that, I tend to use TypeScript aiming to rely heavily on the static type system. I’m trying to pass on it, most of the responsibility for guarding the integrity of the codebase.
I still write tests, but they are higher level and focused only on the business logic.
I can move fast, even when there is a need for a drastic change in the core of the application. Because having static code analysis on my site lets me write fewer low-level tests.
This is especially helpful near the deadline.
I know, it’s shocking… 🙃
What types can do…
Let’s think about a typical web application, it has a backend (BE) and a frontend (FE) with the complexity spread in both parts of the system.
One of the basic, necessary and obvious constraints in successful communication is having BE and FE speak the same language. In other words, the data they exchange needs to be of the same type at both ends of the request.
It’s pretty straightforward to restrict that contract if we structure the project as a TypeScript monorepo, containing packages for both BE and FE.
We keep all the shared utilities and types in a special package named core.
Next, we use the same static type in both places in the codebase, where we send and receive the data.
This is a simple technique, to pass all responsibility for guarding the consistency in the application on the type-system.
Usually, we add an extra layer of security by keeping an eye on this static type contract during PR reviews.
It’s very important to have a structured, well-defined naming convention and approach for our types. Writing clean code is absolutely necessary!
Build time vs Runtime validations
We can use the same pattern when we define communication the other way around, FE sending data to the BE.
But what we’ve used so far, is not enough…
TypeScript works its type-safety magic only during the development phase. When the program runs, the data arriving via HTTP is always of the unknown type. In other words, we have to validate it.
To overcome this issue I like to create validator functions taking input unknown (any) data and returning it as data of a well-known static type.
The essence of this pattern is that validator functions return the same type that is later used in the business logic.
If the validator doesn’t return, it throws a validation error and we don’t handle the request at all. Validators are the only place in our handlers that need to perform these checks.
Let’s assume that for a particular operation we need to get data that corresponds to this type:
This is how we can create a validator for it:
To keep our code DRY we’re taking advantage of the existing tools. They let us declare the type by extracting it from the validator itself.
Removing the need for writing the type declaration and its validation separate.
Thanks to this, there is only one place in the codebase that needs to modify if the requirements change.
One of the solid options to create such validators is io-ts.
The library works great especially with io-ts types, containing codecs like NonEmptyString or DateFromISOString.
During the runtime, codecs can convert data to a known type e.g. string to a Date object e.g. DateFromISOString.
It relies on fp-ts, exacting functional programming style which might not be preferable by your team.
Here you can find what is the idea behind io-ts and more on io-ts-types.
And here is an example of a validator built with io-ts with extraction of a static type from its return type.
If functional programming is not your style, AJV is a well-known library and a great alternative.
It supports JSON Type Definition and has a TypeScript utility type named JTDDataType. It can extract type from a schema definition.
This is pretty cool, and the library can also run more sophisticated validation.
Let the static types do what static types do…
Finally we can focus more on the actual business logic.
The type system will guard that no contract between any parts of the system is broken, on the holistic scope that not many tests can do. It will do it with the faster feedback loop than any tests can do. Giving you detailed, very precise message where the issue is, that only unit tests very close to the implementation details can do.
Stay tuned. Big UP!