Using Zod with TypeScript: A Guide for Frontend Developers

August 17, 2024 (3mo ago)

If you're building a frontend application, you're likely working with data. Whether you're fetching data from an API, handling form submissions, or managing state, you need to ensure that the data you're working with is valid. Enter Zod, your new best friend. In this article, we'll explore how to use this powerful library with TypeScript to validate data in your frontend applications.

What is Zod?

Zod is a TypeScript-first schema declaration and validation library. It allows you to define the shape of your data using a schema and validate that data against that schema. It is designed to be easy to use, type-safe, and performant—making it a great tool for ensuring that the data in your application is valid and consistent. Imagine writing less boilerplate code and letting this library handle the heavy lifting of data validation.

Installing Zod

Requirements

// tsconfig.json
{
  // ...
  "compilerOptions": {
    // ...
    "strict": true
  }
}

To get started, install the library using npm or yarn:

npm install zod

Or if you're using yarn:

yarn add zod

Once installed, you can start importing the library in your TypeScript files and take advantage of its powerful features.

Understanding Primitives

At the core of the library are the primitive types. These are the basic building blocks that you can use to define the shape of your data. The library provides a range of primitive types, including string, number, boolean, null, and undefined. You can use these types to define the structure of your data and validate that it conforms to your expectations.

String

The string type represents a string value. You can use it to validate that a value is a string:

import { z } from "zod";
 
const stringSchema = z.string();

You can also enforce various constraints like minimum and maximum lengths, or specific patterns:

const usernameSchema = z.string().min(3).max(20);
const emailSchema = z.string().email();

Number

Defining numbers is equally straightforward:

const numberSchema = z.number().int().positive();

This ensures that the value is an integer and positive.

Booleans

Booleans are, unsurprisingly, even simpler:

const booleanSchema = z.boolean();

Arrays

The library makes working with arrays a breeze:

const stringArraySchema = z.array(z.string());

This creates a schema for an array of strings.

Objects

Creating complex nested objects is where the library really shines. Here's how you can define an object schema:

const userSchema = z.object({
  username: z.string().min(3),
  age: z.number().positive(),
  email: z.string().email(),
});

With this schema, you can easily validate user data.

Unions

You can also define unions of different types:

const stringOrNumberSchema = z.union([z.string(), z.number()]);

This schema allows values that are either strings or numbers.

Intersections

Intersections allow you to combine multiple schemas:

const userSchema = z.object({
  username: z.string().min(3),
  age: z.number().positive(),
});
 
const emailSchema = z.object({
  email: z.string().email(),
});
 
const userWithEmailSchema = z.intersection([userSchema, emailSchema]);

This schema ensures that the data matches both the user and email schemas.

This is just a taste of what you can do with the library's primitive types. You can combine them in various ways to create complex schemas that match the structure of your data.

Guides and Concepts

So, how do you use these schemas in real-world applications? Let's look at some practical guides and concepts.

Schema Validation

Once you've defined a schema, you can use it to validate data. The library provides a parse method that you can use to validate and parse data:

function createUser(user: unknown) {
  userSchema.parse(user);
  // If the function gets here, the validation is successful
}

If the data doesn't match the schema, the library will throw an error with detailed information about what went wrong.

Optional and Nullable

You can define optional and nullable fields in your schemas:

const userSchema = z.object({
  username: z.string().min(3),
  age: z.number().positive().optional(),
  email: z.string().email().nullable(),
});

The optional method allows the field to be omitted, while the nullable method allows the field to be null.

Handling Errors

The library's error handling is comprehensive and developer-friendly. You can either catch the error thrown by parse or use safeParse to handle it more gracefully:

const result = userSchema.safeParse(userInput);
 
if (!result.success) {
  console.error(result.error.errors);
}

This way, you can log the errors and provide feedback to the user. The error messages are detailed and informative, making it easy to pinpoint the issue.

Transformations

The library allows you to transform data as part of the validation process. You can use the transform method to apply a function to the data before validating it:

const numberStringSchema = z.string().transform(val => parseFloat(val));
 
const val = numberStringSchema.parse("123.45"); // val will be 123.45 as number

This can be useful for normalizing data or converting it to a different format.

Why Use This Library?

The library isn't the only schema validation tool out there. How does it stack up against others like Joi or Yup?

TypeScript-First

One of the most compelling features is its TypeScript-first approach. While other libraries like Yup and Joi have TypeScript support as an afterthought, this one is built with TypeScript in mind, ensuring type safety and coherence.

Ease of Use

When compared to Joi, this library's API is much more intuitive. No more second-guessing method names or parameters; it is straightforward and developer-friendly.

Bundle Size

For frontend developers concerned about bundle size, it has the edge over Joi. The library is lightweight and easy to tree-shake.

Validation and Transformation

Yup is known for its powerful validation and transformation capabilities, but this library is catching up fast. With its chainable API and intuitive methods, it is becoming the go-to option for many developers.

Conclusion

This library is a powerful and versatile tool that makes data validation a breeze. By combining it with TypeScript, you can ensure that your frontend applications are robust, type-safe, and performant. Whether you're validating form data, API responses, or state updates, this tool has you covered. Be it simple primitives or complex nested objects, it can handle it all.

In this article, we've only scratched the surface of what this library can do. I encourage you to explore the documentation and experiment with different schemas to see how it can help you build better frontend applications. Embrace the power of type-safe data validation and let this library simplify your development workflow.

Happy coding!