Zod + TypeScript: One Source of Truth for Your Data
If you’ve ever struggled to keep your TypeScript types, API contracts, and form validations in sync: meet Zod.
In this guide I’ll walk through how to define Zod schemas, validate and transform data at runtime, and then reuse those schemas to infer TypeScript types instead of hand-writing them. We’ll also look at .parse() vs .safeParse(), some practical error handling patterns, and how to plug Zod into React with TanStack Form for real-time validation that doesn’t feel like boilerplate.
The goal isn’t to learn every Zod feature, but to get to a point where your validation logic lives in one place and the rest of your code just benefits from it.
What is Zod?
Zod is a TypeScript-first library for schema validation and type inference. It allows you to define strict schemas for data validation, ensuring type safety in TypeScript projects. By creating a schema in Zod, you automatically get TypeScript types inferred from it, reducing the need to manually write types and ensuring consistency across your application. This means you can focus more on the data structure, and less on type definitions.
Additionally, Zod introduces a unique approach to validation, where you parse data instead of just validating it. This distinction ensures better handling of invalid data by throwing errors that can be caught, providing more control over data integrity.
In this document we are going to work using Typescript + React + Vite, so I highly recommend to create project using the next script:
We are also going to use TanStack Form so we need to install it
Zod v4 is now the stable release. If you're using an older version (v3), it's recommended to upgrade to benefit from the latest features and improvements.
Once we have all installed we can start.
Defining Schemas
If you want to validate something, you need to create a Schema of it.
But, what is a schema?
Schema is a representation of a Type, it can be just a simple primitive type or a complex object with different types on it. Here's an example:
As you can see in this example, we have:
- First name as string
- Last name as an optional string (optional fields in Zod are indicated by
.optional()) - Email as email
- Birthdate as a date
- Age as a number
In Zod v4, you can use z.email() directly for email validation, whereas in Zod v3, you would use z.string().email()—same with z.date() instead of z.string.date().
This user schema can be used to parse data, and check if the app is receiving the correct data type.
However, real-world data isn't always clean. For example, form inputs usually return strings—even when you expect numbers or dates. That's where z.preprocess becomes useful.
You can use z.preprocess to transform the input before validation. For instance, to convert a string into a number or a Date object:
In this example, even if the birthdate or age come as strings (e.g., from a form input), the schema will convert and validate them correctly.
Parsing data
Once you have defined your schema, you need to parse the data against it. Zod provides two main methods for parsing data: .parse() and .safeParse(). These methods allow you to validate and transform the data according to your schema, but they behave differently when handling validation errors.
.parse
You can use .parse(object you want to parse) for any Zod Schema and, if the parse is correct, it will return a strongly-typed deep clone of the input.
For valid inputs it returns:
But, if the input is not valid it throws an instance of ZodError, it needs to be caught to prevent your app from breaking.
For example, in the following object the email is not valid, the date is wrong, and the age is a string.
When parsing this data, we can see the following error:
The error messages can be customized, but we'll jump to it later here.
.safeParse
The key difference between parse and safeParse is the response when they fail. While the parse function throws a ZodError, safeParse always returns a JSON-like result, so you don't need to catch the response:
Failure response example:
Success response example:
When to use .parse() vs .safeParse()
- Use
.parse()when you want to handle validation errors explicitly using try-catch blocks. This is useful when you want to interrupt your code flow on validation failure and immediately handle it. - Use
.safeParse()when you prefer to avoid exceptions and want a more structured response. This is ideal when you want to handle validation errors more gracefully, especially in situations like form validation or asynchronous validation flows.
Now we know about schemas and parsing but, what if we have a type "User" and want to be consistent with the schema? That's where inferred types come in.
Inferred types
Type inference is one of the most powerful features of Zod when working with TypeScript. It lets you define your types just once — in the schema — and automatically infers the corresponding TypeScript types from it.
Advantages of Type Inference in Zod
- Consistency: You only define your schema once, and TypeScript guarantees that the inferred types are consistent across your application.
- Type Safety: Since Zod ensures the type is always derived from the schema, you get robust type safety without extra effort.
- Simplified Code: Avoid manually writing types for your data models, saving time and reducing the potential for errors.
Custom errors
We've covered a lot of useful features, but what if we want to customize error messages for different validations? Fortunately, it's easy! Most Zod functions and schema methods support custom error messages.
There are two ways to define them. The first is by passing the error message directly as a parameter to the field you want to validate. For example:
You can also use an object instead of a string if you want to have more parameters than just the error message:
Both types of declared errors will return the error messages this way:
If you want to display a nicely formatted error message, Zod v4 provides a helper: z.prettifyError(error).
Here's how you can use it:
Log:
Now we are ready to use Zod in an actual application, let's go to our React app and let's apply all of this knowledge.
Using Zod in a Front-End form
Once we have all the app configured we can start by creating a simple form in our App component:
Once we have it, let's define the schema. The form has 4 inputs:
firstNamewith a minimum of 3 characterslastNamewith a minimum of 3 charactersagewith a minimum value of 18 and a maximum of 100emailthat should be a valid one
As you can see, we are using custom error messages on each field, as well as the preprocess function we saw earlier.
Once we have the schema ready, we can move on and add the fields to the form:
In the following example, the Input component receives a field object as a prop. For simplicity, we're typing it as AnyFieldApi, but for a more robust and type-safe implementation, we recommend reviewing the official docs.
This field object provides everything we need to wire the input: its name, value, onChange, and onBlur handlers. It also includes validation state, such as field.state.meta.errors, which we can use to display error messages.
Here's the example:
Now, how can we validate the fields? Well, that's probably the easiest task of this guide. You just need to add 4 lines of code: add a validators field with your schema to the useForm parameter. As long as your field names match the schema, validation will happen automatically!
Why Zod over the alternatives?
There are many schema validation libraries available, like Yup, Joi, or class-validator, and each has its strengths. Joi is battle-tested and widely used in Node.js backends. Yup offers a similar API to Joi but is often used in the frontend with Formik. class-validator integrates well with class-based architectures like NestJS.
However, Zod stands out for being TypeScript-first by design. While libraries like Yup require you to maintain separate type definitions or rely on external tools like yup-infer, Zod automatically infers TypeScript types directly from your schema. This reduces duplication and keeps your code consistent and type-safe by default.
Zod also embraces a functional, composable API that feels more predictable and ergonomic, especially in modern React workflows. It supports powerful features like .preprocess, .transform, custom error messages, and safe parsing, making it not just a validator, but also a data transformation tool.
If your project is TypeScript-based and you value strict type safety, maintainability, and clean syntax, Zod is likely the best fit.
Conclusions
As you've seen in this article, Zod is a powerful library that simplifies schema validation and type inference. You can now be confident that you have a single source of truth, your types and validations stay in sync by defining just one schema.
This significantly reduces the risk of errors. The ability to define custom errors for each field makes form creation straightforward, with everything defined in one place. No more updating types, validations, and error messages across multiple files, with Zod it all lives on one side.