Validating TypeScript Types in Runtime using Zod

Validating TypeScript Types in Runtime using Zod

TypeScript has revolutionized the way we write JavaScript applications by adding static type definitions. These type definitions help catch errors early during the development phase, leading to more robust and maintainable code. However, TypeScript's type checking only occurs at compile-time, leaving a gap when it comes to runtime validation. This is where Zod, a powerful schema declaration and validation library, comes into play.

Why Use Zod?

While TypeScript ensures that your code adheres to the specified types during compilation, it does not perform any type checks at runtime. This can lead to unexpected errors, especially when dealing with external data sources like API responses or user inputs. Zod addresses this limitation by providing runtime type-checking capabilities, making sure that the data conforms to the expected types even after the code has been compiled.

With Zod, you can define schemas that describe the shape and type of your data. These schemas can then be used to validate incoming data at runtime, ensuring that the data matches the expected format before it is processed by your application.

Setting Up Zod

Before we dive into the details of using Zod, let's set it up in a TypeScript project.

Installation

You can install Zod using npm or yarn by running the following commands:

npm install zod

Or, if you prefer yarn:

yarn add zod

Configuration

To get the most out of Zod, make sure your tsconfig.json is configured to enable strict type-checking. Here's an example configuration:

{
  "compilerOptions": {
    "strict": true
  }
}

Basic Usage of Zod

Zod makes it easy to define and validate schemas for various data types. Let's start with some basic examples.

Defining Simple Schemas

To define a schema for a simple data type like a string, you can use the following code:

import { z } from 'zod';

const stringSchema = z.string();

Validating Data with Schemas

Once you have defined a schema, you can use it to validate data using the parse method. If the data does not match the schema, Zod will throw an error. Here is an example:

const result = stringSchema.parse('Hello'); // Valid

If you try to validate data that does not match the schema, Zod will throw a ZodError:

try {
  stringSchema.parse(123); // Throws ZodError
} catch (error) {
  console.error(error.errors); // Output validation errors
}

Advanced Usage of Zod

Zod is not limited to simple data types; it can handle complex objects, arrays, and even custom validations. Let's explore some of these advanced features.

Defining Object Schemas

You can define schemas for objects by specifying the structure and types of their properties:

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(), // Optional property
});

const validUserData = {
  username: 'johnsmith',
  email: 'john@example.com',
  password: 'strongpassword123',
};

try {
  const myUser = UserSchema.parse(validUserData);
  console.log(myUser);
} catch (error) {
  console.error(error.errors);
}

Arrays and Tuple Schemas

Zod allows you to define schemas for arrays and tuples. For example, to validate an array of strings or a tuple containing a string and a number:

const stringArray = z.array(z.string());
const tupleSchema = z.tuple([z.string(), z.number()]);

try {
  const arrayResult = stringArray.parse(['apple', 'banana']); // Valid
  const tupleResult = tupleSchema.parse(['hello', 42]); // Valid
  console.log(arrayResult, tupleResult);
} catch (error) {
  console.error(error.errors);
}

Nested and Composed Schemas

You can create more complex schemas by nesting objects and composing schemas using methods like .extend() and .merge():

const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  zipCode: z.string().length(5, 'Invalid zip code'),
});

const UserWithAddressSchema = UserSchema.extend({ address: AddressSchema });

const userDataWithAddress = {
  username: 'johndoe',
  email: 'john@example.com',
  password: 'securepassword',
  address: {
    street: '123 Main St',
    city: 'Metropolis',
    zipCode: '12345',
  },
};

try {
  const user = UserWithAddressSchema.parse(userDataWithAddress);
  console.log(user);
} catch (error) {
  console.error(error.errors);
}

Custom Validations and Refinements

Zod allows for custom validation logic using the .refine() method. This is useful for adding validation rules that go beyond the basic schema definitions:

const PositiveNumberSchema = z.number().refine((val) => val > 0, {
  message: 'Number must be positive',
});

try {
  const number = PositiveNumberSchema.parse(10); // Valid
  console.log(number);
} catch (error) {
  console.error(error.errors);
}

try {
  PositiveNumberSchema.parse(-5); // Throws ZodError
} catch (error) {
  console.error(error.errors);
}

Transformations

The .transform() method allows you to modify data during validation. This can be useful for normalizing inputs or applying business logic:

const trimmedStringSchema = z.string().transform((val) => val.trim());

try {
  const trimmed = trimmedStringSchema.parse('  hello  '); // Transforms to 'hello'
  console.log(trimmed);
} catch (error) {
  console.error(error.errors);
}

Practical Applications and Examples

Zod is highly versatile and can be applied to various scenarios. Here are some practical applications.

API Response Validation

When interacting with external APIs, it's crucial to ensure that the data received matches the expected structure. Zod can be used for this purpose:

const response = await fetch('https://api.example.com/data');
const data = await response.json();

try {
  const parsedData = UserSchema.parse(data);
  console.log(parsedData);
} catch (error) {
  console.error('Invalid API response:', error.errors);
}

Form Validation in React

Zod can be integrated with React to validate form inputs. Here's an example of a simple form validation:

import React, { useState } from 'react';
import { z } from 'zod';

const formSchema = z.object({
  username: z.string().min(3, 'Username must be at least 3 characters'),
  email: z.string().email('Invalid email'),
});

function App() {
  const [formData, setFormData] = useState({ username: '', email: '' });
  const [errors, setErrors] = useState({ username: '', email: '' });

  const validate = () => {
    try {
      formSchema.parse(formData);
      setErrors({ username: '', email: '' });
      return true;
    } catch (error) {
      if (error instanceof z.ZodError) {
        const newErrors = error.errors.reduce((acc, err) => {
          acc[err.path[0]] = err.message;
          return acc;
        }, {});
        setErrors(newErrors);
      }
      return false;
    }
  };

  const handleChange = (e) => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validate()) {
      console.log('Form data is valid:', formData);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Username</label>
        <input name="username" value={formData.username} onChange={handleChange} />
        {errors.username && <span>{errors.username}</span>}
      </div>
      <div>
        <label>Email</label>
        <input name="email" value={formData.email} onChange={handleChange} />
        {errors.email && <span>{errors.email}</span>}
      </div>
      <button type="submit">Submit</button>
    </form>
  );
}

export default App;

Type Inference

Zod also provides type inference, allowing you to extract TypeScript types from schemas. This can be useful to avoid duplicating type definitions:

const UserSchema = z.object({
  username: z.string(),
  email: z.string(),
});

type User = z.infer<typeof UserSchema>;

const validateUser = (user: User) => {
  // user is now strongly typed
};

Conclusion

Zod is an indispensable tool for TypeScript developers, providing robust runtime type-checking to complement TypeScript's compile-time checks. By defining schemas and validating data at runtime, you can ensure that your application handles data consistently and correctly, leading to more reliable and maintainable code. Whether you are validating API responses, user inputs, or complex objects, Zod offers a versatile and intuitive solution.

For further reading and resources, you can refer to the official Zod documentation and explore various community contributions and tutorials to deepen your understanding of Zod and its applications.

11/4/2024
Related Posts
Validating API Response with Zod

Validating API Response with Zod

Learn why validating API responses with Zod is indispensable for TypeScript apps, especially when handling unexpected data formats from third-party APIs in production.

Read Full Story
How to Use Zod to Get Structured Data with LangChain

How to Use Zod to Get Structured Data with LangChain

Struggling with getting structured data from LLM? Discover how to leverage LangChain and Zod for bulletproof AI outputs.

Read Full Story
How to Use Zod Validation for React Hook Forms

How to Use Zod Validation for React Hook Forms

Discover how to seamlessly integrate Zod validation with React Hook Form, ensuring robust, type-safe validation for your forms. A step-by-step guide from setup to advanced techniques!

Read Full Story