Content
- Content
- Introduction
- Understanding the why
- Managing contracts
- Traditional approach
- The Zod Way
- Simple example
- Establishing a new standard - End-to-end Typesafety
- Striking the right balance
- Conclusion
Introduction
I recently came across a neat little library called Zod.
My first reaction looking through the documentation was this looks interesting.
It wasn’t until I tried it that I felt the difference — The difference is MASSIVE.
Nothing comes close to it.
It’s a different approach but once you try out Zod, I think you would know what I mean.
In my opinion, Zod’s approach hits the right balance between robust code, and developer experience (DX) when working with data validation in Typescript.
⚠️ Disclaimer: After reading this article, you may not want to use any other validation library! (You’ve been warned)
Understanding the why
Typescript introduces great checks during development by statically checking the code meets the “contract” defined in the types.
This works for most cases, however, in production, it becomes more complex.
Javascript - Typescript does not run in production, it compiles down to Javascript
Loose contracts - Typescript type contracts are not enforced when code is compiled to Javascript
Data predictability - Data quality and sources tend to be un-predictable in complex system running in production
Hence, for this reasons, there is a need for run-time validation to enforces these contracts within Javascript.
Managing contracts
When we work with functions in Javascript, it consists of inputs and outputs.
A certain set of inputs will give you certain set of outputs — This is what I call a contract.
It is not too different from a contract you sign a contract with your bank or insurance or telecommunication company.
There is a certain level of guarantee when you sign on for their services.
By establishing a contract it forces us to narrow the scope of the inputs and outputs.
In essence, you reduce the surface area hence making the function more predictable.
Now comes the question, how is this done in Javascript ?
Traditional approach
The traditional approach to achieve this is installing some sort of validation library (ie Joi, Ajv etc).
The most common application for this is managing form inputs with user input data.
However, it doesn’t have to be only for forms, you can use run-time validation for anything.
It will make your code more robust because any sort of data not meeting a contract will be considered a failure.
There is not in between or edge cases. It makes the code very strict.
The trade off with these libraries is there is a lot of duplication - like A LOT in a large code base.
Not only do you have to define the Typescript types, you also have to define validation schemas.
Talk about doubling the work...
If you ever needed to do this, you know this pain. Also, let’s not even get into how much this bloats up the code base 😵.
Then next thing you know, you start doing this 👇
Well, is there a better way ?
What if I told you there is...
You can probably guess it. I’ll give you a hint, it start with a Z.
The Zod Way
Enter Zod.
Here is where Zod differs from all the other validation libraries.
How is it different from everything else ? Zod takes a schema first approach.
Meaning, you start with your validation schema (Zod schema).
Then, this Zod schema becomes your validations, and your types.
So, you get the best of both worlds!
Not only do you you get run-time validations from the schema but you also get the types by converting the schema into Typescript.
Neat huh ? Talk about super charging productivity and developer experience 😍⚡️
Simple example
Enough of the illustrations, I want to see some code!
Let’s go through a simple example.
Let’s say we’re a pizza shop, and we need to design some schemas for our website.
1. Defining the Zod schema
import { z } from 'zod';
// Zod schema
const pizzaSchema = z.object({
sauce: z.string(),
ingredients: z.array(z.string()),
});
- Convert Zod schema into Typescript types
import { z } from 'zod';
// Zod schema
const pizzaSchema = z.object({
sauce: z.string(),
ingredients: z.array(z.string()),
});
// TypeScript type
export type IPizza = z.infer<typeof pizzaSchema>;
- Create some pizzas
import { z } from 'zod';
// Zod schema
const pizzaSchema = z.object({
sauce: z.string(),
ingredients: z.array(z.string()),
});
// TypeScript type
export type IPizza = z.infer<typeof pizzaSchema>;
const pepperoniPizza: IPizza = {
sauce: 'tomato',
ingredients: [
'cheese',
'pepperoni',
],
};
console.log(pepperoniPizza);
// => { sauce: 'tomato', ingredients: [ 'cheese', 'pepperoni' ] }
const hawaiianPizza: IPizza = {
sauce: 'tomato',
ingredients: [
'cheese',
'pineapple',
'ham',
],
};
console.log(hawaiianPizza);
// => { sauce: 'tomato', ingredients: [ 'cheese', 'pineapple', 'ham' ] }
- Run-time validations
import { z } from 'zod';
// Zod schema
const pizzaSchema = z.object({
sauce: z.string(),
ingredients: z.array(z.string()),
});
// TypeScript type
export type IPizza = z.infer<typeof pizzaSchema>;
const pepperoniPizza: IPizza = {
sauce: 'tomato',
ingredients: [
'cheese',
'pepperoni',
],
};
console.log(pizzaSchema.parse(pepperoniPizza));
// => { sauce: 'tomato', ingredients: [ 'cheese', 'pepperoni' ] }
const hawaiianPizza: IPizza = {
sauce: 'tomato',
ingredients: [
'cheese',
'pineapple',
'ham',
],
};
console.log(pizzaSchema.parse(hawaiianPizza));
// => { sauce: 'tomato', ingredients: [ 'cheese', 'pineapple', 'ham' ] }
console.log(pizzaSchema.parse(null)); // throws ZodError
Striking the right balance
Most libraries will force you to the right thing by sacrificing developer experience (DX).
That’s not the case with Zod, the team really got it ‘just right’.
When using Zod, you can do the right thing without any friction at all.
It works seamlessly with Typescript.
Now that’s a tool worth looking into.
Establishing a new standard - End-to-end Typesafety
Zod opens the door up to interesting tools like tRPC which takes the developer experience (DX) to the next level.
The big idea with tRPC is you can define a backend endpoint with a schema, then have automatically have autocompletion on the client side.
This raises the standard for all other frameworks to provide integrations with tRPC or create a “similar” experience.
I see tRPC, and “tRPC like experiences” being more prevalent in the future merely for the speed of development and developer experience (DX) it provides.
Conclusion
So, there you have it. That’s Zod.
A library that gives you this seamless experience for designing robust code using both run-time (schema) and static (types) validations.
Before we go, let’s do a recap.
And... that’s all for now, stay tuned for more!
If you found this helpful or learned something new, please do share this article with a friend or co-worker 🙏❤️ (Thanks!)
Also published at - jerrychang.ca
⚠️ Note: Yup also supports the ability to infer types from the data schema defined. You can do something like yup.InferType in order to get your Typescript type. Just throwing it out there as another great library that allows you to do similar things as Zod.
Top comments (31)
I think it's very good, but after testing your application made with TypeScript and seeing that the functions (contracts) receive the required types of parameters, and that everything works perfectly, I don't think it's necessary to add that to your production file; Because for the end user, if a ZODerror occurs, the only thing you should show is "there has been an error, try again later", and that's it.
For the end user it doesn't make sense to add Zod.js because, besides you add size to the final file by adding that.
Although the overhead is not much (only 8 kb according to the README), I am still in favor of not adding what is not needed in production.
At least in the environment that I have in mind, maybe in an application that is very crazy and that communicates with many APIs and these return arbitrary data, maybe if necessary.
zod schemas have a safeParse method to easily have error handling and not throw an error.
also it's just zod not zod.js
Sure, but the problem Zod is supposed to solve is the lack of type-checking in JavaScript in production. And let's just say it's not needed in most applications either.
If you have already developed your application with TypeScript and tested it several times, and all the functions receive the parameters they should receive, and everything works perfectly, in production it should also work the same. And if it doesn't, it doesn't matter to the user, it doesn't matter if it's with Zod, the same "An error has occurred, try again" window will still be shown.
zod is not for type checking, its for validation. you're completely misunderstanding the purpose of zod. let's say you have a form for email and password. you can define a zod schema to validate this:
now you can validate that the user has entered a valid email and a password that is at least 8 characters long. if it doesn't match those requirements, zod will throw an error and you can handle it accordingly.
i would recommend reading the zod documentation and learning about validation on both client and server side in general. validation !== type checking. runtime safety !== type safety
Oh thanks. Now I understand, seeing the visuals and reading the blog post I understood that it was for typechecking, but now it seems to me that it is okay to include it in production 🤗.
Great discussion! 👏
Please don't ever use zod for anything else than input validation. Addin extra verification to you code in production is a huge performance degrade. You have Rest error codea or GraphQL errors for BE contracts, using zod to validate typings it's a nice trick, but not usefull for anybody. If I see that kind of over engineering in a pull request, we are going to have a talk.
I do agree on the performance. However, in my opinion, it’s all situational.
If we’re really talking about performance, then you can just code in Go, Java, C#.
There is no hard and fast rule for anything, especially in production systems. It’s all a trade off.
We’re talking about coding & running in javascript in production here.
It has many flaws compared to C# and Java.
Pass by Reference - It’s easy to mutate the objects in JS, leads to un-predictable results in legacy code bases
Data integrity vs performance - It’s easier to fix a performance issue than a data integrity issue
Weird behaviors - Many weird behaviours with coercing and issues with data, numbers
As an author of a library 30x faster than Zod, I can still say that Zod is fast enough to not care about it. These are literally nanoseconds. And if we're talking about consuming REST, GraphQL APIs, then it should be handled with codegen.
Hi @tonyb, I believe you can!
That’s what I like about Zod is that it is very flexible.
Without typescript:
Then if you decide later that you want to use typescript, just use:
This looks pretty nice, I'm going to have to give it a shot. I've long felt that by lacking runtime checks, TypeScript doesn't really live up to its full potential, though if you're really strict about type narrowing with assertions and if-statements (as opposed to casting with
as
) you can actually get a decent degree of runtime type enforcement that way.Totally! It’s a great tool.
Check our their documentation - github.com/colinhacks/zod
I didn’t run into any issues trying it out.
@aminnairi isn't it what you were looking for all these times? 😅
Zod is basically my new girlfriend now
Married to the type safety 😂🤣
Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍
This is a lot of ugly code intended as a hack to get around that TypeScript checks types at compile time only, not at runtime. I'll wait for runtime type checking.
You're probably way over-engineering things. And the hype factor is huge here. Do you work for Zod?
I tried Zod and many others long ago and eventually concluded that it was not worth the trouble. But don't worry! Devs love to complicate things! Makes them feel important (or is it impotent? I get those confused). I'm an outlier.
My advice: before you add any dependency, consider whether you really need that bloat and if there isn't an easier way to do it using already-available tools. For example, HTML forms have built-in validation. Who knew?
I never heard of that, I was too busy touching grass.
Oh, is that legal now?
Is there any plans for TypeScript to ever add a runtime for type checking?
How do you prevent errors at runtime for when an API call does not send back the correct data type?
Genuine questions, I'm really curious about your feedback, thanks!
Frankly, I don't know. That would require browsers to parse TypeScript, no? As TS is a proprietary language (Microsoft), I'm not really in favor of that.
What I am in favor of is TC-39 getting off their collective asses and implementing a decent, optional Hindley-Milner sort of type system with inferencing in the language. It could be a simple as a specific type of comment. For example, why not:
This keeps the code itself very clean (TS types can make code very difficult to read). It also allows me to curry the function:
I don't need to type the inner function because it is inferred from the outer one. This seems very clean and workable to me, and any smart type system designer could come up with a good one in a month or two. So why the hell is it taking YEARS to get here? Simple answer: politics.
I don't see any reason why you can't type variables the same way, preferably with better types than JS currently has!
This is much less cognitive load than the current TS approach. Initially,
int32
anduuid
could be cast tonumber
orbigint
internally until runtimes could add the appropriate types. Of course, if BigInt is anything to go by, that will probably be around 2050 or so...Smart editors, such as VSCode, should have a simple toggle for comments that allows you to toggle them on and off. And/or for types -- that's needed for TS right now! Why can't I hide them when I'm not needing them so I can focus on the algorithm itself without all the type noise, toggling them back on when I need to check the type (or seeing the type on hover)?
I have built multiple validation systems, including a few that use composition and a JSON configuration to build validators from potentially deeply nested validations. Here is an example from one I built last year:
(That's beta code, so no promises.)
You call
makeAfterAlphabetically
with the string that you want to test your input against. For example:The call to
afterPearAlphabetically
above does not error, so it returns the Validation unchanged:{ datatype: "string", value: "pumpkin" }
. (constraintType
is used to choose this function, but it passed because it appears in the error, below.)But if I call it with "peach", I get an error:
This acts a bit like a monad. You can pass it from validator to validator and it will add the errors to the errors array (note that they can be nested as well, which is later flat mapped). There are also AND and OR validators, so I can do something like this in my configuration:
The above creates a single
validate
function that checks that the value to be tested meets:These can be infinitely nested (but why?). I included static type checking, which allowed me to extend the type system to include integers, fractions, etc. Here's a list, to give an idea of the power of this approach:
This was all part of a larger rendering system which essentially took a JSON object representing the DOM to be created, and then recursed through it to render the HTML elements, including form components. This part of a simple data-driven UI concept -- the GraphQL response builds the UI and the front end code is generic and small (can be statically-generated , too, of course). Simpler than React (and faster).
The "validators" are really just operators and can be combined with operators such as
makeDivide
(division) andmakeEven
(check input is an even number). And formatters such asmakeAsCurrency
ormakeAsCreditCardNumber
(which can themselves validate).These can be used not only for form validation, but for conditional display of any part of the UI. Whole sections of pages can be conditionally displayed based on a boolean expression of any complexity. And the values used can be injected from anywhere -- the URL, session storage, a fetch call, another form field, whatever -- at compile time, at render, or even at runtime. Operands can themselves be operations, so you can nest.
This is a purely FP, immutable, composable approach. Some thinking remains about security -- this works as essentially a big eval function, so must think it through more. Also, need some way to tree shake so unneeded functions aren't loaded unnecessarily.
Obviously, there is too much here to give this a full explanation (if anyone has read this far). I built this for a company as a proof of concept, but it was just too much for the other devs and I think they abandoned it after my contract ended. Meanwhile I got interested in using a triple store and an ontology instead of a JSON config to generate the application, front and back, and am now (very slowly) working on that.
But a whole lot of that code could be avoided if a) JS had decent types, and b) JS had a solid, optional, runtime type checker with inferencing.
But I'm not expert on type systems so maybe there's something I'm missing.
Actually, I copied the operations module out into its own repo, MIT licensed, so you're welcome to take a closer look: github.com/site-bender/operations. Happy to answer questions if you have any.
What I'd really like is a type system like that of Idris, only better. What if we could do something like this:
So this takes a string of any length (n) and another of length (m) and produces a string that must be of length n + m.
And why can't we not only specify and integer, but also a prermitted range? Then the system could choose the right sized int automatically, no?
That's off the top of my head, but I'm sure smarter folks could come up with much better ideas. And if this checked at runtime, too, maybe we won't need validation at all.
Or dates?
This would mean that endDate has to be after startDate and the return value is a positive integer.
Hey, a man can dream...
Wondering, how can you speak so about Zod, as if it's overcomplicated and useless, after developing multiple validation systems which are doing the same with a different syntax?
You prefer nested JSON structures as in AJV, and many people prefer DSL that Zod provides. The same with TS: you prefer writing types in comments, people prefer writing types in the actual code.
There are alternatives to TS: PureScript, ReScript, Hegel, Elm. You're free to use them, and leave TS to be as people like it.
I haven't gone back to reread my post, but I don't remember saying that Zod was useless. Overcomplicated yes.
I have used Elm and PureScript, both eventually seemed more trouble than they were worth. And while I wasn't thrilled with some of the Elm naming choices, I was even less thrilled when they decided to dilute it a bit. In the end, I decided that both were more trouble than they are worth. But that only affects my choices so I don't see how that is anyone else's concern.
The validation system I was experimenting with (described in a previous comment) is written in TypeScript, so I am somewhat confused by your comment that I should "leave TS to be as people like it". I suggested that JS should have its own Hindley-Milner type system and not be dependent on a private company (Microsoft) for its types (or Meta and flow). I recommended that the types use comments so they can be backwards compatible, ignored on older systems, and that they be inferred.
At no point did I suggest changing TypeScript, and I have no ability to do so even if I wanted to. The only think I said about TS, as I recall, was that it would be nice if editors such as VSCode would let you toggle the visibility of the types on and off. Most of the time they are just superfluous noise, and they could be shown (as they often are) on hover.
I don't actually prefer nested JSON structures. That was just the first thing I tried. The code I described was simply a proof of concept. Actually, I am more interested in using SHACL/OWL together with a triple store and SPARQL queries to generate the HTML/CSS/JS directly.
The point of the proof of concept was to look at taking a purely functional approach and composing pure functions that passed around a Validation object sort of like a monad. The code may be somewhat more complex -- because it is fairly abstracted -- but I only need to write it once and then I can reuse it endlessly. YMMV. I presented it as an alternative.
I'm sorry your feelings are hurt, but you've grossly misrepresented my comments. I don't think you really read them.
Are you kidding? :)
I was polite and said nothing bad about you, only expressed my surprise about your comments and shared some thoughts.
That's a cheap dirty move to go in personal confrontation. An indicator that you're not the person one would want to discuss something with.
I am not going to comment further to add more gas to the fire 😂
Either way, I am going to archive this thread. It’s going into a direction that is not positive.
If you want to discuss other libraries, feel free to start your own post and comment there.
Please keep the discussions strictly about the topic at hand - Zod.
Please no personal comments.
Thank you.
How about
io-ts
? It was my first library for schema validation with typescript support. I'm sureio-ts
andzod
are not the only libs with such functionality.I can’t say much about
io-ts
because I haven’t use it.How is using
io-ts
?I am sure there are other libraries out there...
yup
is also a good one.Zod looks interesting to me because many others in the teams are adopting it Astro & tRPC.
If you used bitdowntoc to generate the TOC, I am so sorry 🙏. There is a little bug that breaks the links when the title contains dashes. For now on, you can fix it manually by removing the dashes from the generated anchor link in the TOC (the one with the word end-to-end and the one with run-time), or by checking the "generate anchors" option in the UI. I am actively working on a fix, so please don't stop using it! 😊
Have you ever seen this:
github.com/ferdikoomen/openapi-typ...
Some comments have been hidden by the post's author - find out more