Schema First Type Design in Typescript
In this issue, I lay down my thoughts and ideas on the concept of schema first design approach in Typescript, where we generate our Typescript types from schemas which act as a single source of Truth.
For a while now, I have meant to write about this issue to collect and convey my thoughts on Schema First Type design in Typescript. I will be giving this talk for the This is Learning virtual conference, and I thought it would be amazing to have an issue accompanying the talk—killing two birds with one stone.
I have also covered this topic in the relationship between two libraries - Valibot and Zod- and this will be a broader look, as we can achieve this with a lot more.
So, without further ado, let’s get going:
Schema-first type design?
Instead of designing our types in Typescript, this idea is to start somewhere else and then generate static types.
Typescript types, generally, represent the state of our data, defining what we expect our data to be. So, for instance, if we had a list of users, we expected to have a type describing our user with all the fields we needed for that specific user. This is the data that Typescript takes, and type checks out code against it.
The one issue with Typescript is that during the transpilation/compilation (choose your poison) process, these Types are stripped away as Typescript gets transpiled to Javascript. This also means we don’t have access to the type of definition we spent a lot of time designing at runtime.
So, if we need runtime validation, such as user input validation, we would need a different tool to achieve that. Validating user input and external data is important; we have tools like data validation schemas to help us with this.
Problem?
So, to recap, we have Types and schema validation schemas that basically define the same data. One is used for type-checking, while the other is used for data validation at runtime.
This is a lot of duplication, and it can lead to a situation where our schemas and our types are out of sync as your codebase scales, which could have unforeseen consequences.
If you like my content and want to support my work, please consider supporting me (you can buy me a double latte, the juice that powers my creativity and determination ☕️) through Github Sponsors.
Solution
So, what are the solutions? What if we could start validation schemas and then generate a typescript? Typescript schemas have several benefits, such as being more detailed and fine-grained.
With schemas, we can check data that fits a certain pattern, such as email, phone, etc., among other checks, which is difficult to achieve with different types. So, the only thing we need to do is generate static types.
We have a few options for schema tools that we can use as our single source of truth for our Typescript types. In this issue, I will briefly examine GraphQL and Zod/Valibot as alternatives to GraphQL.
GraphQL
GraphQL is strongly typed and can be a great source of truth for describing data for our API. It has strong data validation built in and can be extended. While we can’t use a GraphQL schema directly for types, we can use it as a basis to generate Typescript types that we can use within our application, reducing code duplication and having a single source of truth.
This can be achieved using tools such as graphql-codegen; we can generate types and add more annotations to the schema to provide better types that reflect what we want our types to look like but centrally located.
In addition, you can use the GraphQL code generation tool to generate JSON schemas for data and form validation. This would allow us to achieve a single source of truth for our application and not worry about the effects of code duplication we get by maintaining different schemas and types.
Schema Data Validation Libraries - Zod and Valibot
While GraphQL is great, not everyone uses it. Zod and Valibot allow us to define schema validation schemas for data validation in our application. We can also get static types from both, which Typescript can use for type-checking within your application.
Here is an example of me using Zod to get Typescript Types from the schema.
The beauty of the above code snippet is that we can use it to validate data at runtime in places like forms. On top of that, by parsing the data, we get automatic type narrowing. Zod will throw an error if it can’t validate the data—remember to handle that.
The same can be achieved using Valibot, which I covered in a previous issue for All Things Typescript earlier this year.
Zod and Valibot are not the only two tools that can help you achieve this, but for this issue, I wanted to convey an idea and am not confident enough to recommend tools that I have tried and used previously. Still, there are a lot of good options out there for you to consider, and I will be taking a look at each in upcoming issues.
Conclusion
In this issue, I laid down my thoughts on the concept of schema first design in Typescript. This is a concept where instead of designing Typescript types ourselves, we start with a schema such as a GraphQL schema or a data validation schemas and generic static types form them. This reduces code duplication and ensures we have a single source of truth for our types, ensuring that our code scales in the future.
Let me know what you think in the comment section below.
That's it for this issue. Thank you for getting this far. If you enjoyed this article and would like to support my work, please share and like this issue and consider sharing All Things Typescript with friends and colleagues.
Did you know you can also hire me to coach your team and help them improve their Typescript skills? If this sounds interesting to you, please consider getting in touch so we can work something out.
And until next time, please keep on learning.
Have you looked at ways to generate Typescript types from OpenAPI specs? Often you need to create an OpenAPI spec for your users anyway, so this seems like a promising approach to me. But I haven't tried it myself yet.