A TypeScript type disappears at compile time. The form, the API response and the configuration file, meanwhile, arrive at runtime, and nothing guarantees they look like what was announced.
Zod fills that gap: one schema genuinely checks the data and supplies its type.
Definition
Zod is a schema validation library. You describe the expected shape by assembling rules, then confront an unknown value with that description. What passes is conformant, what fails is described line by line.
import { z } from "zod";
const Invoice = z.object({
id: z.number().int().positive(),
email: z.email(),
amount: z.number().min(0),
status: z.enum(["draft", "paid"]),
});
const result = Invoice.safeParse(incomingData);
if (!result.success) {
console.log(result.error.issues);
// [{ path: ["id"], message: "Too small: expected number to be >0" }]
}Each rule chains onto the previous one and narrows the accepted set a little further. It reads left to right: a number, an integer, strictly positive.
Two ways to fail
| Method | What it does on failure |
|---|---|
| parse | Raises an error, to be caught with a try |
| safeParse | Returns an object whose success property is false |
The first suits places where failure is abnormal, such as an environment variable missing at startup. The second suits everywhere failure is expected, typically a form, since the messages have to be displayed rather than the program interrupted.
The type comes from the schema
Writing the schema and then retyping the type beside it creates two truths that will drift apart. Zod derives the second from the first, which guarantees they can no longer contradict each other.
type Invoice = z.infer<typeof Invoice>;
// { id: number; email: string;
// amount: number; status: "draft" | "paid" }After a successful safeParse, the data property already carries that type. Runtime verification and compile-time knowledge therefore come from the same place, which is exactly what TypeScript alone was missing.
Frequently asked questions
Where should validation sit?
At every boundary of the program: API response, incoming request body, form, environment variables, contents of a JSON file. Inside, once the data has been validated, types are enough. Validating the same value twice in the middle of the chain only slows the program down.
Does Zod replace TypeScript?
No, the two answer different questions. TypeScript checks the coherence of the written code, before execution, at no cost once compiled. Zod checks a real value during execution, at a cost. They complement each other all the better because the second can produce what the first needs.
What about the error messages?
The default messages are written in English and for a developer: fine for logs, not for a user. Every rule accepts a custom message, which allows translating and rewording case by case. The issues array carries the path of each offending field, enough to place the message under the right form field.