TypeScript Typeguard Transparency
For those familiar with TypeScript, you may already know about typeguards and how useful they can be. If you are not familiar with typeguards, i’d recommend checking them out. There are plenty of guides and resources available on the topic:
- https://basarat.gitbook.io/typescript/type-system/typeguard
- https://rangle.io/blog/how-to-use-typescript-type-guards/
- https://2ality.com/2020/06/type-guards-assertion-functions-typescript.html
While there are many great resources already available, most of the examples given deal with pretty small data structures or only deal with the validity of one field.
At my current place of employment, we use typeguards heavily to verify that API responses are what we expect them to be and that objects are of the right shape and have valid data before saving them into our database.
At this scale however, typeguards start to become too opaque for my liking… Especially when dealing with larger data models and nested structures.
Let’s look at an example of type Car
and it’s subtypes of Engine
and Wheel
:
Now, what if the obj we are checking has an improper type nested in the wheel object? Or maybe the engine object has a fuelType
of deisel
rather than diesel
because of some small typo somewhere in the codebase. While the typeguard will do its job, declaring that the object is not of type Car, trying to debug this situation will quickly become a nightmare. If you are throwing errors on failed type checks, you will likely see something like “Object x does not match the schema for Car”. At this point you will likely be thinking to yourself “uhhhh… yes it does, what the hell??”
The real pitfall here is that if one field is invalid, the whole check fails with no indication of why. This will then inevitably force you to start logging out the entire object so you can try to see where the invalid field(s) are… Not fun.
So, having encountered a number of these bugs in our system, I grew very frustrated with how little transparency our typeguards gave into the reasoning behind failed type checks. Following is the solution I came up with.
First, we have two generic helpers, RuleSet and isType.
RuleSet is a generic interface that defines a key and a function to check if that key is valid. The key in the rule set should match exactly to the key in the interface.
isType is a generic type check function that will take the object to check and the rule set to check against. The nice thing here is that isType will log out any key and value that fails the check.
Given our Car
type example, we can change all of our typeguard functions (isCar
, isEngine
, isWheel
) to rule sets.
As you can see, our rule sets look almost exactly like our typeguard functions with the benefit of actually being easier to read and define (at least in my opinion).
Now if we have a small typo, like our engine model having fuelType
as deisel
instead of diesel
we get a message that points out exactly why the object is invalid:
Conclusion
I hope you found this article valuable and that it saves you time debugging in the future!
Source code & tests can be found here: https://gist.github.com/armueller/ac31bfe290f256eda830ea495d3426dd