Validate All the Things with Zod

Article summary

It is important to validate any external data when using Typescript. You are not guaranteed that the data is what you think it is by casting. One way to validate your data is by using type guards:

type Foo = {
    x: string;
}
const isAFoo = (x: unknown): x is Foo => {
    return "x" in x;
}

This works fine for simple objects, but what about more complex objects? Or objects with other nested objects? This can become complex and hard to maintain fast if not kept in check. This is where schema validation comes into play.

Schema Validation

Schema validation allows you to define a schema that can be used to validate data. This can range from making sure an object has the right keys with the right types to making sure a number fits into certain bounds. Ever since discovering it, I’ve been relying heavily on schema validation to provide type safety in my code.

There are many different schema validation libraries, each with its own pros and cons. I’ve recently been using Zod and have been enjoying it due to its “developer-friendly” API. As a side note: I’m not trying to convince you that Zod is the best schema validation library, so if you have any reasons to use other validation libraries, please let me know in the comments!

Examples

Let’s refactor the type guard example above to use Zod as our first example:

import { z } from 'zod'

const fooSchema = z.object({
  x: z.string(),
});

type Foo = z.infer<typeof fooSchema>;

const x: Foo = fooSchema.parse({ x: "hello" });
// this throws
const y: Foo = fooSchema.parse({ z: "hello" });

Zod allows us to define a schema, infer types from that schema, then validate based on that schema. If the data doesn’t match with the schema, Zod will throw an error. Simple enough right?

We can also use Zod to validate that primitives match certain properties:

// validates that a string is an email
const emailSchema = z.string().email();
// validates that a number is greater than 0 and less than 100
const lessThan100Schema = z.number().gt(0).lt(100);

Moving back to objects, Zod also makes it easy to define unions and intersections:

const fooSchema = z.object({
  type: z.literal("foo"),
  x: z.number(),
});

type Foo = z.infer<typeof fooSchema>;

const barSchema = z.object({
  type: z.literal("bar"),
  y: z.number(),
});

type Bar = z.infer<typeof barSchema>;

type FooBar = Foo | Bar;

const fooBarSchema = z.union([fooSchema, barSchema]);

fooBarSchema.parse({ type: "foo", x: 1 });
fooBarSchema.parse({ type: "bar", y: 1 });
// this throws
fooBarSchema.parse({ type: "foo", y: 1 });
// so does this
fooBarSchema.parse({ type: "bar", x: 1 });

<h2Conclusion

Since Zod makes validation so easy, I don’t see a reason not to validate all of my data using it. If you want peace of mind for all of your data, I highly recommend integrating Zod (or another schema validation library) into your project!

If I’ve piqued your interest with Zod, then I would check out tRPC. tRPC is tightly integrated with Zod to allow you to define an API with built-in validation for all of your inputs and outputs.

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *