Having Fun with TypeScript: Zod

Schema Validation Made Easy

Dagang Wei
3 min readMar 12, 2024
Source

This blog post is part of the series Having Fun with TypeScript.

Introduction

In the world of TypeScript development, ensuring data integrity and predictability is paramount. That’s where Zod shines — a powerful schema declaration and validation library that gives your applications an extra layer of robustness. In this blog post, we’ll dive into what Zod is, why it’s an essential tool, and how to get started.

What is Zod?

At its core, Zod lets you define schemas that meticulously describe the shape and types of your TypeScript data. Think of these schemas as blueprints for your data structures. Zod then uses these blueprints to validate any data you throw at it, ensuring it conforms to the expected format. If there’s a mismatch, Zod will provide clear and informative error messages.

Why Zod Matters

  1. Type Safety Elevated: Zod acts as a guardian for your TypeScript types. When you define a Zod schema, Zod automatically infers a corresponding TypeScript type. This seamless integration dramatically reduces the need for repetitive type definitions.
  2. Runtime Validation: Zod isn’t just about types; it actively validates data at runtime. This is crucial when you’re dealing with external sources like user input, API responses, or database entries — situations where you can’t fully trust the incoming data.
  3. Developer Experience: Zod is praised for its developer-friendly syntax. Creating schemas feels intuitive and natural, leading to cleaner, more maintainable code.

Installation

npm install zod or yarn add zod

Key Features

Basic

Let’s imagine you’re building a user registration form. Here’s a Zod schema to validate the form data:

import { z } from 'zod';

const UserSchema = z.object({
username: z.string().min(5, 'Username must be at least 5 characters'),
email: z.string().email('Invalid email format'),
password: z.string().min(8, 'Password must contain at least 8 characters'),
age: z.number().optional(),
});

// Type inference in action
const validUserData = {
username: 'johnsmith',
email: 'john@example.com',
password: 'strongpassword123'
};

const myUser = UserSchema.parse(validUserData);

// TypeScript infers the type of 'myUser' as:
// { username: string; email: string; password: string; age?: number }

parse vs. safeParse: Safety Nets for Data Validation

Zod offers two primary methods for validation:

  • parse: Throws an error if validation fails. Use this when you have high confidence in the data’s structure and want to catch potential errors early.
  • safeParse: Returns a result object containing either the parsed data on success or an error object on failure. This is ideal when dealing with less predictable data, allowing you to gracefully handle validation issues.
const userInput = { username: 'jane', email: 'not-an-email' };

// parse will throw an error
try {
UserSchema.parse(userInput);
} catch (error) {
console.error("Validation failed:", error);
}
// safeParse returns a result
const result = UserSchema.safeParse(userInput);
if (!result.success) {
console.error("Validation failed:", result.error);
}

Advanced

Here’s a code snippet demonstrating .transform() and .refine() in action, along with their usage:

import { z } from 'zod';

const UserSchema = z.object({
username: z.string().min(5, 'Username must be at least 5 characters')
.transform(username => username.trim().toLowerCase()), // Transformation
email: z.string().email('Invalid email format'),
password: z.string().min(8, 'Password must contain at least 8 characters'),
zipCode: z.string().refine(
(value) => value.length === 5 && /^\d{5}$/.test(value),
'Invalid zip code format'
) // Refinement
});

type User = z.infer<typeof UserSchema>;

// Usage
const untidyInput = {
username: ' Alice75 ',
email: 'alice@Example.com', // Incorrect casing
password: 'verystrongpassword',
zipCode: '12345'
};

const parsedUser = UserSchema.parse(untidyInput);

console.log(parsedUser);
// Output:
// { username: 'alice75',
// email: 'alice@example.com',
// password: 'verystrongpassword',
// zipCode: '12345'
// }

Explanation

  • Schema: The UserSchema includes both .transform() (for the username to lowercase and trim whitespace) and .refine() (for the zipCode validation).
  • Input Data: The untidyInput represents data that might come from a user form submission.
  • Parsing: UserSchema.parse() validates the data and applies the transform to the username.
  • Output: The output shows the parsed and cleaned data, ready to be safely used in your application.

Key Points

  • Clarity: This code snippet emphasizes the difference between cleaning up data (transformation) and adding extra validation rules (refinement).
  • Data Integrity: Zod ensures that user-provided data is structured correctly, standardized, and safe to use.

Conclusion: Let Zod Be Your Data Guardian

If you’re serious about TypeScript development, Zod is a must-have tool in your arsenal. Its ability to strengthen type safety and streamline data validation will save you countless hours of debugging and lead to more robust applications.

--

--