Schema validation in TypeScript with Zod
Tags: typescript, webdev
Published: 2022-11-02 17:11:00

**Written by [Abhinav Anshul](https://blog.logrocket.com/author/abhinavanshul/)✏️**

TypeScript is awesome! It has improved developer productivity and tooling over recent years. [TypeScript](https://www.typescriptlang.org/) not only helps with static type checking, but also has added a set of object-oriented programming (OOP) concepts such as generics, modules, classes, interfaces, and more. 

Arguably, going back to a JavaScript-only codebase can be difficult if you have worked with TypeScript. Although TypeScript looks great in all aspects, it has a blind spot — it only does static type checking at compile time and doesn't have any runtime checks at all. 

That’s where Zod comes in. In this article, you will learn about schema design and validation in Zod and how to run it in a TypeScript codebase at runtime.

*   [What is Zod and why do we need it?](#what-zod-why-need-it)
*   [Why use Zod over other tools?](#why-use-zod-other-tools)
*   [Benefits of using Zod with TypeScript](#benefits-using-zod-typescript)
*   [Primitives in Zod](#primitives-zod)
*   [Objects in Zod](#objects-zod)
*   [Composing complex schema objects](#composing-complex-schema-objects)
*   [Type inferences in Zod](#type-inferences-zod)

## What is Zod and why do we need it? <a name="what-zod-why-need-it">

You might be asking yourself, why would someone need a runtime check in the first place? 

Well, runtime checks help in getting correctly validated data on the server side. In a case where the user is filling out some kind of form, TypeScript doesn't know if the user inputs are as good as you expect them to be on the server at runtime. 

Therefore, Zod helps with data integrity and prevents sending out garbage values to the database. Also, it’s better to log an error on the UI itself, like in cases where a user typed in numbers when you expect a string. 

[Zod](https://zod.dev/) is a tool that solves this exact problem. It fills this TypeScript blindspot and helps with type safety during runtime. Zod can help you build a pretty flexible schema design and run it against a form or user input.

## Why use Zod over other tools? <a name="why-use-zod-other-tools">

[There are already a couple of tools such as Yup](https://blog.logrocket.com/comparing-schema-validation-libraries-zod-vs-yup/), Joi, io-ts, and more that do the same thing as Zod. While all these libraries are great for schema designing and doing runtime validation in general, Zod outshines itself in several factors such as:

*   Flexibility: Zod is very flexible and can chain several instances together, thereby preventing duplication of static types
*   It has no dependencies
*   Although it is best to use Zod along with TypeScript, if you want a limited, Zod-only type safety, you can use it in plain JavaScript projects as well. It is not TypeScript dependent
*   It focuses on immutability, therefore some methods like `optional()` return a new instance instead of mutating the same object altogether

## Benefits of using Zod with TypeScript <a name="benefits-using-zod-typescript">

Zod works particularly well with TypeScript. In a typical TypeScript codebase, you ensure that all the static type safety will be handled by TypeScript at compile time. Even most of the third-party packages ship their code along with their `types`, like how React has its types under an npm package called `@types/react`. 

Having an additional runtime check using Zod fixes these issues with TypeScript. You might have encountered issues where you need to assign `undefined` or `unknown` to a type even after using its corresponding `@types` package — the sole reason being you don't know beforehand what a user would enter or what the response structure is like. 

Zod applies these runtime checks in a very concise way. That is literally like taking data types from TypeScript and extending them to Zod-specific methods. Zod automatically infers the type that you have mentioned already in TypeScript, preventing type duplication. 

These kinds of chainable Zod's utility methods have inspired TypeScript utilities as well, proving how well these two go along together. 

Additionally, there are situations where TypeScript’s error handling would be suboptimal and Zod could do a better job displaying the error at runtime, based on a user’s interaction with the UI.

## Primitives in Zod <a name="primitives-zod">

Let's start talking about schema validation with a very basic Zod example:

```javascript
import { z } from "zod"
const dataInputFromUser = z.string().min(8).max(16)
dataInputFromUser.parse("A long text")
```

The above code will safely parse. You can navigate the user to the next input or a page, depending on your use case. 

And if we tweak the line a bit, like this:

```javascript
dataInputFromUser.parse("A really long text")
```

It'll throw an exception:

```javascript
errors: [
    {
      code: 'too_big',
      maximum: 16,
      type: 'string',
      inclusive: true,
      message: 'String must contain at most 16 character(s)',
      path: []
    }
  ]
```

If you need safer exception handling, you can simply log the error using the `.safeParse()` method. 

This is one of the simplest examples of using primitives in Zod. Primitive values are not just limited to `string`, but provide other methods such as `number`, `bigint`, `boolean`, and `date`. There are a couple of empty types as well like `undefined`, `null`, and `void`. 

Utilizing these primitives along with a few specific methods can lead to a very flexible schema design:

```javascript
// email validation //
z.string().email().startsWith(string).trim().max(18).min(1)
// can be used for Terms & Conditions check at runtime //
z.boolean()

// can be used for high-precision large values if they are being calculated at runtime //
z.bigint()
```

The example above is chaining these primitives together to create a very functional type safety at runtime for an email input field.

## Objects in Zod <a name="objects-zod">

Most of the user-facing form requires a couple of data inputs and validation of varying data types. This is where it's better to use objects in Zod. You can create a schema for a set of properties you want at a runtime check.

```javascript
import { z } from 'zod'

const FormData = z.object({
  firstName: z.string().min(1).max(18),
  lastName: z.string().min(1).max(18),
  phone: z.string().min(10).max(14).optional(),
  email: z.string().email(),
  url: z.string().url().optional(),
});

const validateFormData = (inputs: unknown) => {
  const isValidData = FormData.parse(inputs);
  return isValidData;
};
```

In the above TypeScript code, there is no way to enforce schema validation at runtime using TypeScript only, hence the `inputs: unknown`. 

This is where `z.Object()` can be used for building an extensible schema validation. The data will be safely parsed if a user enters the fields that satisfy your schema definition. You'll then be able to send that data to the server. Else, an exception will be thrown, as we saw in the beginning.

## Composing complex schema objects <a name="composing-complex-schema-objects">

There could be such cases where the schema object design is nearly the same for a couple of forms/fields, apart from a few extra or missing types. In those situations, you don't need to keep duplicating the same object schema over and over again. Instead, you can prevent duplication by utilizing Zod methods such as `merge()` and `extend()`. 

The code above can be further improved by removing the duplication for `firstName` and `lastName` types, like so:

```javascript
const GenericStringContraint = z.string().min(1).max(18),

const FormData = z.object({
  firstName: GenericStringContraint, 
  lastName: GenericStringContraint, 
  // ...
});
```

This is similar to a chunk of data types being repeated in a couple of forms. Say, for example, `userId` and `fullName` get repeated in `Article` and `UserProfie` schema definitions, then we can simply "extend" these two as:

```javascript
const UserData = z.object({
  userId: z.string().min(1).max(5),
  fullName : z.string().min(1).max(18),
});

const Article = UserData.extend({
  title: z.string().min(5),
 date: z.date()
});

const UserProfile = UserData.extend({
  isVerifield: z.boolean(),
  numberOfArticles: z.number().positive()
});
```

The above approach can be a better way to have maintainable code and avoid data duplication. One thing to be mindful of while using `extend()` is that it mutates the schema objects, i.e., overwrites them. 

Very similar to the `extend()` method, Zod provides a `merge()` method as well, which differs slightly. It is useful while merging two schema objects, not necessarily "extending" them:

```javascript
import { z } from "zod"

const User = z.object({
  url: z.string().email().min(8),
  name: z.string(),
  age: z.number().min(2).max(3).optional()
})

const Skill = z.object({
  title: z.string().min(1),
})
const SkilledUser = User.merge(Skill)

const Data = SkilledUser.parse({
  url: "nancy@gmail.com",
  name: "Nancy",
  age: 21,
  title: 'water surfing',
})

console.log(Data) // parses successfully
```

As you can see in the code above, the `title` data field has been merged to the `User` schema object, hence why it’d be parsed safely by Zod. In a case where the `title` field is a number, say `20`, Zod would throw an error mentioning that `title` is constrained to the type of `z.string().min(1)` only. 

Zod's `merge()` property doesn't rewrite the fields as it is in the case with `extends()`.

## Type inferences in Zod <a name="type-inferences-zod">

Suppose you already have your type defined somewhere and you want a newly created variable to deduce its type from existing ones. In that case, Zod has a method that can infer its type, as:

```javascript
let fullName = z.string(); 
type fullName = z.infer<typeof fullName> // string

const userAge: fullName = 12; // Zod throws a Type Error Exception
const name: fullName = "Nancy"; // Parses safely
```

## Conclusion

There are more nitty-gritty APIs that Zod provides out of the box, but we have covered almost all the basic entities of building a schema validation with Zod and TypeScript. We also covered how powerful these primitives can be, and, if chained together, can build a very robust and flexible schema design for your application. 

Having a runtime validation is always a good idea, as it makes your code more readable and sanitizes user inputs before redirecting them to the server. 

Zod emphasizes the DRY principle and makes sure that schema defined in one place can be tokenized and used for other types as well, either by inferring those types or extending them. This makes Zod stand out from other similar libraries out there. 

Pairing Zod with a powerful compile type check system as TypeScript can make applications robust in current JAMstack applications.

---
## [LogRocket](https://lp.logrocket.com/blg/signup): Full visibility into your web and mobile apps

[![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/signup)

[LogRocket](https://lp.logrocket.com/blg/signup) is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

[Try it for free](https://lp.logrocket.com/blg/signup).