One Schema → Validation AND Types
Runtime Types with Zod
TS types are erased at runtime — useless for validating untrusted input. Zod gives you a schema that both validates AND infers the TS type.
What you'll learn
- Define a Zod schema
- Validate at the system boundary
- Use `z.infer` to derive the TS type
TS types vanish at compile time. They can’t validate user input, JSON over the wire, or env vars. For that you need runtime validation. Zod is the standard tool — one schema gives you both the runtime check AND the inferred TS type.
Install
npm install zod A Schema
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().int().min(0),
});
type User = z.infer<typeof UserSchema>;
// { id: string; email: string; age: number } z.infer<typeof Schema> gives you the TS type — no duplication.
Validating Input
const raw = JSON.parse(rawJson); // unknown shape
const result = UserSchema.safeParse(raw);
if (!result.success) {
console.error(result.error.format());
return;
}
const user: User = result.data; // typed AND validated safeParse returns a discriminated union — { success: true, data: User }
or { success: false, error }. No throws.
Validation Where It Matters
The right place for validation is the system boundary — where data first enters your trust zone:
- HTTP request bodies
- Form input
localStorage/sessionStoragevalues you read back- Parsed JSON from a file or API
- Env vars
Inside your code — past that boundary — trust the types.
Composition
Schemas compose like types:
const PostSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1),
author: UserSchema, // nested
tags: z.array(z.string()),
});
const PostListSchema = z.array(PostSchema); Refinements
const PasswordSchema = z
.string()
.min(8)
.refine(p => /[A-Z]/.test(p), "must contain an uppercase letter")
.refine(p => /[0-9]/.test(p), "must contain a digit"); Custom checks that go beyond shape.
Why Not Just TS?
Because TS types don’t exist at runtime. A JSON.parse of an
attacker-controlled payload that’s typed as User is a security
bug. Zod fixes that gap.
Up Next
Migrating a JS codebase to TS — the gradual path.
Migrating From JS →