How we set up Runtime type-checking for our TypeScript application
What do you love most about TypeScript? I bet it’s probably Static type checking.
What do you dislike most about TypeScript? It could also be Static type checking.
We want Runtime type checking
TypeScript is great at detecting potential errors during compile time with its static type checking. When TypeScript has a sense of the data type, it can identify most issues during compilation.
Since it only has static type checking. It can’t perform type-checking at runtime. And, the problem is that applications often don’t have data until they are actually running. They rely on external sources to receive incoming data.
It really fails us with external data that comes during runtime. Take the following snippet as an example.
This code will definitely break. However, we are not shown any errors because we are asserting the type as Hero in TypeScript.
We have failed successfully!
TypeScript fixes the problem. Almost…
But don’t worry! TypeScript has a solution for this problem called Type Predicates. It lets us set up a function to validate the type of data in runtime.
Great, now we were able to save our app from becoming a disaster. But sadly, we are also trading off the excellent Dev Ex that TypeScript offers.
We begin by asserting the type as unknown
and then using type predicates to validate the type while maintaining a type for it. Furthermore, every time I change the type, I need to remember to update the Predicate as well.
Argh, it ends up being overly complex code. Additionally, determining the possible types that a variable or expression could take on can be difficult in some cases and requires careful consideration.
Simply put; this solution is not robust.
Zod to the rescue
Zod is a schema validation library that gives us the robust solution we are searching for.
Let’s jump in to see how Zod does that.
In the snippet above we see a schema at the top. And inside the try
block, external data is passed to the parse()
method, which performs the validation.
Our job is simply to define the schema and use it to parse the data. If the data we receive is invalid, Zod will throw an error that needs to be handled.
And my favorite part of this approach is, we just have to maintain the schema. Everything else will update automatically.
It means that Zod gives us the superpower of inferring types from the schemas we define to use them elsewhere. Like below,
Awesome, right?
But, wrapping the entire data inside a try-catch
block is also not a good design.
However, a good design is where we get rid of all invalid data at its origin. Therefore, runtime type validation should only be performed at the entry points of the application.
Collecting input from the user, reading from LocalStorage, and getting data from API are the places where we often rely on external data for our work at Vaya. To ensure the integrity of the data, we use type checking as gatekeepers, encapsulated in neatly packaged helper functions like the one shown in the following snippet:
Zodios allows us to take this approach to next level by providing end-to-end type-safety which I’m excited to try. And our Python backend at Vaya uses Pydantic for the runtime type checking. The Python + Pydantic combo gives the data both flexibility and maintainability at the same time.
Runtime type checking doesn’t have to be a blocker to do wonders.
Finally, if you think Zod is nothing new and that Yup has been doing this work for a long time with a more mature ecosystem, wait for my next article. I’ll delve deeper into the fundamental differences between Zod and Yup, and explain why we ultimately chose Zod.
At Vaya, we have a culture of setting up exciting goals and solving for blockers that get in the way. There are a lot more stories like this to share so follow us here. And if you’re someone who likes to work on problems like this, we’d like to hear from you. Please get in touch with me on LinkedIn.
Thank you so much Yashi for the editorial support!