TypeScript Zod Integration
Use Zod schemas as the single source of truth for runtime validation and TypeScript types
When to Use
- Validating API request bodies, query parameters, or form data
- Deriving TypeScript types from validation schemas (single source of truth)
- Parsing and transforming unknown data from external sources
- Building type-safe environment variable configuration
Instructions
- Define a schema and infer the type:
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().cuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['user', 'admin']),
createdAt: z.coerce.date(),
});
type User = z.infer<typeof UserSchema>;
// { id: string; email: string; name: string; role: 'user' | 'admin'; createdAt: Date }
- Parse unknown data — validates and returns typed result:
const user = UserSchema.parse(untrustedData);
// Throws ZodError if validation fails
const result = UserSchema.safeParse(untrustedData);
if (result.success) {
console.log(result.data); // Type: User
} else {
console.log(result.error.issues); // Validation errors
}
- Compose schemas with pick, omit, extend, and merge:
const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });
const UpdateUserSchema = UserSchema.partial().required({ id: true });
const UserWithPostsSchema = UserSchema.extend({
posts: z.array(PostSchema),
});
- Transform data during parsing:
const InputSchema = z.object({
email: z
.string()
.email()
.transform((e) => e.toLowerCase().trim()),
tags: z.string().transform((s) => s.split(',').map((t) => t.trim())),
age: z.coerce.number().int().positive(),
});
- Validate environment variables:
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
API_KEY: z.string().min(1),
});
export const env = EnvSchema.parse(process.env);
// Fails at startup if any required env var is missing or invalid
- Use with API route handlers:
// Next.js Route Handler
const QuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
search: z.string().optional(),
});
export async function GET(request: Request) {
const url = new URL(request.url);
const query = QuerySchema.parse(Object.fromEntries(url.searchParams));
// query is fully typed: { page: number; limit: number; search?: string }
}
- Discriminated union schemas:
const EventSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
z.object({ type: z.literal('keypress'), key: z.string() }),
z.object({ type: z.literal('scroll'), offset: z.number() }),
]);
type Event = z.infer<typeof EventSchema>;
- Custom validation with
refineandsuperRefine:
const PasswordSchema = z
.object({
password: z.string().min(8),
confirm: z.string(),
})
.refine((data) => data.password === data.confirm, {
message: 'Passwords do not match',
path: ['confirm'],
});
- Error formatting:
const result = UserSchema.safeParse(data);
if (!result.success) {
const formatted = result.error.flatten();
// { formErrors: string[], fieldErrors: { email?: string[], name?: string[] } }
}
Details
Zod provides runtime validation that TypeScript's type system cannot. TypeScript types are erased at runtime — they cannot validate data from APIs, databases, or user input. Zod bridges this gap by generating both validation logic and TypeScript types from a single schema definition.
Schema-first approach: Define the Zod schema first, then derive the TypeScript type with z.infer. This eliminates the common problem of types and validation drifting apart.
parse vs safeParse:
parsethrowsZodErroron failure — use in contexts where invalid data is an error (middleware, startup validation)safeParsereturns a discriminated union{ success: true, data: T } | { success: false, error: ZodError }— use when you need to handle validation failure gracefully
Performance: Zod validation is faster than JSON Schema for most schemas. For very high-throughput paths (>10K validations/second), consider valibot or compile schemas with @anatine/zod-openapi.
Zod with Prisma: Libraries like zod-prisma-types auto-generate Zod schemas from Prisma models:
generator zod {
provider = "zod-prisma-types"
}
Trade-offs:
- Zod schemas add runtime overhead — validate at system boundaries, not inside tight loops
z.infertypes can be complex in IDE tooltips — use type aliases for readabilitytransformchanges the output type —z.inferreflects the OUTPUT type, not the input- Zod is synchronous by default — use
parseAsyncfor schemas with async refinements
Source
https://typescriptlang.org/docs/handbook/2/types-from-types.html
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.