DEV Community

Cover image for Zod: The Next Biggest thing after Typescript

Zod: The Next Biggest thing after Typescript

Jerry on January 08, 2023

Content Content Introduction Understanding the why Managing contracts Traditional approach The Zod Way Simple example Defining the Z...
Collapse
 
leober_ramos33 profile image
Leober Ramos

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.

Collapse
 
nexxeln profile image
Shoubhit Dash

zod schemas have a safeParse method to easily have error handling and not throw an error.

also it's just zod not zod.js

Collapse
 
leober_ramos33 profile image
Leober Ramos

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.

Thread Thread
 
nexxeln profile image
Shoubhit Dash • Edited

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:

const formSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});
Enter fullscreen mode Exit fullscreen mode

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

Thread Thread
 
leober_ramos33 profile image
Leober Ramos

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 🤗.

Thread Thread
 
jareechang profile image
Jerry

Great discussion! 👏

Collapse
 
ebitzu profile image
eBitzu • Edited

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.

Collapse
 
jareechang profile image
Jerry

Please don't ever use zod for anything else than input validation.

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.

  1. Pass by Reference - It’s easy to mutate the objects in JS, leads to un-predictable results in legacy code bases

  2. Data integrity vs performance - It’s easier to fix a performance issue than a data integrity issue

  3. Weird behaviors - Many weird behaviours with coercing and issues with data, numbers

Collapse
 
dzakh profile image
Dmitry Zakharov

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.

Collapse
 
jareechang profile image
Jerry

Hi @tonyb, I believe you can!

That’s what I like about Zod is that it is very flexible.

Without typescript:

const z = require('zod').z;

// Zod schema
const pizzaSchema = z.object({
  sauce: z.string(),
  ingredients: z.array(z.string()),
});

const pepperoniPizza = {
  sauce: 'tomato',
  ingredients: [
    'cheese',
    'pepperoni',
  ],
};

console.log(pizzaSchema.parse(pepperoniPizza));
Enter fullscreen mode Exit fullscreen mode

Then if you decide later that you want to use typescript, just use:

// My type
type IPizza = z.infer<typeof pizzaSchema>;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mistval profile image
Randall

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.

Collapse
 
jareechang profile image
Jerry

Totally! It’s a great tool.

Check our their documentation - github.com/colinhacks/zod

I didn’t run into any issues trying it out.

Collapse
 
anwar_nairi profile image
Anwar

@aminnairi isn't it what you were looking for all these times? 😅

Collapse
 
aminnairi profile image
Amin

Zod is basically my new girlfriend now

Collapse
 
jareechang profile image
Jerry

Married to the type safety 😂🤣

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍

Collapse
 
chasm profile image
Info Comment hidden by post author - thread only accessible via permalink
Charles F. Munat

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?

Collapse
 
jareechang profile image
Jerry

HTML forms have built-in validation. Who knew?

I never heard of that, I was too busy touching grass.

Collapse
 
chasm profile image
Charles F. Munat

Oh, is that legal now?

Collapse
 
aminnairi profile image
Amin

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!

Collapse
 
chasm profile image
Charles F. Munat

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:

//+ string -> string -> string
function concatenate (left, right) {
  return left + right
}
Enter fullscreen mode Exit fullscreen mode

This keeps the code itself very clean (TS types can make code very difficult to read). It also allows me to curry the function:

//+ string -> string -> string
function concatenate (left, right) {
  function concatTo(right) {
    return left + right
  }

  return arguments[1] == null ?  concatTo : concatTo(right)
}

concatenate("rama", "lama")            // "ramalama"
concatenate("ding", "dong")            // "dingdong"

const concatToJoe = concatenate("joe") // concatTo
concatToJoe(" mama")                   // "joe mama"
Enter fullscreen mode Exit fullscreen mode

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!

//+ int32
const usPopulation = 334_274_321

//+ uuid
const id = Crypto.randomUUID()
Enter fullscreen mode Exit fullscreen mode

This is much less cognitive load than the current TS approach. Initially, int32 and uuid could be cast to number or bigint 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:

import makeOperator from '../../../../makeOperator'
import type {
    AfterAlphabeticallyConstraint,
    Validation,
} from '../../../../types/constraints'
import { Operation } from '../../../../types/operations'
import type { StringValue } from '../../../../types/values'
import localeCompareSupportsLocales from '../../../utilities/localCompareSupportsLocales'
import makeError from '../../../utilities/makeError'

export default function makeAfterAlphabetically(
    constraint: AfterAlphabeticallyConstraint,
): (validation: Validation) => Validation {
    const { operand } = constraint
    const injector = (operand as Operation).operatorType
        ? makeOperator(operand as Operation)
        : () => operand

    return function afterAlphabetically(validation: Validation): Validation {
        const { language = 'en', options } = constraint

        const injected = injector()
        const testValue =
            typeof injected === 'string' ? injected : (injected as StringValue).value

        /* istanbul ignore next */
        const order = localeCompareSupportsLocales()
            ? ((testValue as string) || '').localeCompare(
                    validation.value as string,
                    language,
                    options,
              )
            : ((testValue as string) || '').localeCompare(validation.value as string)

        return order < 0
            ? validation
            : makeError(
                    validation,
                    constraint,
                    `come after ${testValue} alphabetically`,
              )
    }
}
Enter fullscreen mode Exit fullscreen mode

(That's beta code, so no promises.)

You call makeAfterAlphabetically with the string that you want to test your input against. For example:

export type AfterAlphabeticallyConstraint = {
  readonly constraintType: TypeOfConstraint.AFTER_ALPHABETICALLY
  operand: Operation | StringValue | string
} & LocaleCompareOptions

const afterPearAlphabetically = makeAfterAlphabetically({
  constraintType: TypeOfConstraint.AFTER_ALPHABETICALLY,
  operand: "pear"
})

afterPearAlphabetically({
  datatype: "string",
  value: "pumpkin",
})
Enter fullscreen mode Exit fullscreen mode

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:

{
  datatype: "string",
  errors: [
    constraint: {
      constraintType: TypeOfConstraint.AFTER_ALPHABETICALLY,
      operand: "pear"
    },
    error: "AFTER_ALPHABETICALLY_ERROR",
    errors: [],
  ],
  isInvalid: true,
  value: "peach",
}
Enter fullscreen mode Exit fullscreen mode

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:

const constraint = {
  constraintType: TypeOfConstraint.AND,
  tests: [
    {
      constraintType: TypeOfConstraint.IS_INTEGER,
    },
    {
      constraintType: TypeOfConstraint.OR,
      tests: [
        {
          constraintType: TypeOfConstraint.AND,
          tests: [
            {
              constraintType: TypeOfConstraint.AT_LEAST_N,
              operand: 7,
            },
            {
              constraintType: TypeOfConstraint.LESS_THAN_N,
              operand: 11,
            },
          ],
        },
        {
          constraintType: TypeOfConstraint.AND,
          tests: [
            {
              constraintType: TypeOfConstraint.MORE_THAN_N,
              operand: 21,
            },
            {
              constraintType: TypeOfConstraint.AT_MOST_N,
              operand: 42,
            },
          ],
        },
      ],
    },
  ],
}
Enter fullscreen mode Exit fullscreen mode

The above creates a single validate function that checks that the value to be tested meets:

(value >= 7 && value < 11) || (value > 21 && value <= 42)
Enter fullscreen mode Exit fullscreen mode

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:

  • alphabetical
    • makeAfterAlphabetically
    • makeBeforeAlphabetically
    • makeNotAfterAlphabetically
    • makeNotAfterAlphabetically
  • composers
    • makeAnd
    • makeOr
    • makeXor
  • date
    • makeAfterDate
    • makeBeforeDate
    • makeOnOrAfterDate
    • makeOnOrBeforeDate
  • datetime
    • makeAfterDateTime
    • makeBeforeDateTime
  • length
    • makeAtLeastNCharacters
    • makeAtMostNCharacters
    • makeAtExactlyNCharacters
    • makeFewerThanNCharacters
    • makeMoreThanNCharacters
  • number
    • makeAtLeastN
    • makeAtMostN
    • makeEqualToN
    • makeLessThanN
    • makeMoreThanN
    • makeNotEqualToN
  • other
    • makeConfirmation
    • makeMatch
  • sequence
    • makeIsOrderedList
    • makeIsReversedList
  • set
    • makeIsDisjointWith
    • makeIsMemberOf
    • makeOverlapsWith
    • makeIsSubsetOf
    • makeIsSupersetOf
  • static (I use Temporal)
    • makeIsArray
    • makeIsBoolean
    • makeIsDate
    • makeIsDateTime
    • makeIsDuration
    • makeIsFraction
    • makeIsInstant
    • makeIsInteger
    • makeIsList
    • makeIsMap
    • makeIsMember
    • makeIsMonthDay
    • makeIsPrecision
    • makeIsReal
    • makeIsSet
    • makeIsString
    • makeIsTimeZone
    • makeIsYearMonth
    • makeIsZonedDateTime

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) and makeEven (check input is an even number). And formatters such as makeAsCurrency or makeAsCreditCardNumber (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.

Thread Thread
 
chasm profile image
Charles F. Munat

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:

//+ String n -> String m -> String n+m
function concat(left, right) {
  return left + right
}
Enter fullscreen mode Exit fullscreen mode

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?

//+ Integer n{0,100} -> Integer m{0,10}
function divideByTen(n) {
  return n / 10
}
Enter fullscreen mode Exit fullscreen mode

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?

//+ ZonedDateTime d -> ZonedDateTime d2>d -> Integer n>=0
function getDifferenceInDays (startDate, endDate) {
  return startDate.until(endDate, { largestUnit: "day" }).days
}
Enter fullscreen mode Exit fullscreen mode

This would mean that endDate has to be after startDate and the return value is a positive integer.

Hey, a man can dream...

Thread Thread
 
romeerez profile image
Roman K

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.

Thread Thread
 
chasm profile image
Charles F. Munat

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.

Thread Thread
 
romeerez profile image
Roman K

I haven't gone back to reread my post

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.

I'm sorry your feelings are hurt, but you've grossly misrepresented my comments. I don't think you really read them.

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.

Thread Thread
 
jareechang profile image
Jerry

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.

Collapse
 
olvnikon profile image
Vladimir

Nothing comes close to it.

How about io-ts? It was my first library for schema validation with typescript support. I'm sure io-ts and zod are not the only libs with such functionality.

Collapse
 
jareechang profile image
Jerry

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.

Collapse
 
derlin profile image
Lucy Linder

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! 😊

Collapse
 
leon0824 profile image
Leon

Have you ever seen this:
github.com/ferdikoomen/openapi-typ...

Collapse
 
ashuto7h profile image
Ashutosh Sahu

Class validator and class transformer are still my favourite choice

Some comments have been hidden by the post's author - find out more