Zod Infer
Derive TypeScript types from Zod schemas with z.infer, z.input, and z.output - single source of truth for both runtime validation and compile-time safety.
Search across all documentation pages
Derive TypeScript types from Zod schemas with z.infer, z.input, and z.output - single source of truth for both runtime validation and compile-time safety.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card - copy-paste ready.
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(["admin", "editor", "viewer"]),
createdAt: z.string().datetime().transform((s) => new Date(s)),
});
// z.infer = z.output - the type AFTER transforms
type User = z.infer<typeof UserSchema>;
// { id: string; name: string; email: string; role: "admin" | "editor" | "viewer"; createdAt: Date }
// z.input - the type BEFORE transforms (what you pass in)
type UserInput = z.input<typeof UserSchema>;
// { id: string; name: string; email: string; role: "admin" | "editor" | "viewer"; createdAt: string }
// z.output - same as z.infer
type UserOutput = z.output<typeof UserSchema>;When to reach for this: Whenever you have a Zod schema and need a matching TypeScript type - for props, state, API payloads, database rows, or function signatures.
"use client";
import { useState } from "react";
import { z } from "zod";
// Schema is the single source of truth
const TodoSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1, "Title required").max(200),
completed: z.boolean().default(false),
priority: z.coerce.number().int().min(1).max(5).default(3),
tags: z.array(z.string()).default([]),
});
// Derive all types from the schema
type Todo = z.infer<typeof TodoSchema>;
type TodoInput = z.input<typeof TodoSchema>;
// Create schema - omit auto-generated fields
const CreateTodoSchema = TodoSchema.omit({ id: true });
type CreateTodoInput = z.input<typeof CreateTodoSchema>;
// Update schema - everything partial except id
const UpdateTodoSchema = TodoSchema.partial().required({ id: true });
type UpdateTodo = z.infer<typeof UpdateTodoSchema>;
export function TodoManager() {
const [todos, setTodos] = useState<Todo[]>([]);
const [error, setError] = useState("");
function addTodo(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const fd = new FormData(e.currentTarget);
const raw: CreateTodoInput = {
title: fd.get("title") as string,
completed: false,
priority: fd.get("priority") as unknown as number,
tags: (fd.get("tags") as string).split(",").filter(Boolean).map((t) => t.trim()),
};
const result = CreateTodoSchema.safeParse(raw);
if (!result.success) {
setError(result.error.issues.map((i) => i.message).join(", "));
return;
}
const newTodo: Todo = { ...result.data, id: crypto.randomUUID() };
setTodos((prev) => [...prev, newTodo]);
setError("");
e.currentTarget.reset();
}
return (
<div className="max-w-md space-y-4">
<form onSubmit={addTodo} className="flex flex-col gap-2">
<input name="title" placeholder="Todo title" className="rounded border p-2" />
<input name="priority" type="number" min={1} max={5} defaultValue={3} className="rounded border p-2" />
<input name="tags" placeholder="Tags (comma-separated)" className="rounded border p-2" />
<button type="submit" className="rounded bg-blue-600 px-4 py-2 text-white">Add</button>
{error && <p className="text-sm text-red-600">{error}</p>}
</form>
<ul className="space-y-1">
{todos.map((t) => (
<li key={t.id} className="rounded border p-2 text-sm">
<strong>{t.title}</strong> - Priority: {t.priority} - Tags: {t.tags.join(", ") || "none"}
</li>
))}
</ul>
</div>
);
}What this demonstrates:
z.infer for the validated output typez.input for the raw input type (before defaults and transforms).omit(), .partial(), .required()z.infer<typeof Schema> extracts the TypeScript type that parse() returns (the output type)z.input<typeof Schema> extracts the type that parse() accepts (before transforms and defaults)z.input and z.output are identical.optional(), .nullable() can all cause input and output to differ.pick(), .omit(), .extend(), .partial()) produce new schemas with correctly inferred typesDeriving CRUD types from a single schema:
const ItemSchema = z.object({
id: z.string().uuid(),
name: z.string(),
price: z.number().positive(),
updatedAt: z.date(),
});
type Item = z.infer<typeof ItemSchema>;
type CreateItem = z.infer<typeof ItemSchema.omit({ id: true, updatedAt: true })>;
type UpdateItem = z.infer<typeof ItemSchema.partial().required({ id: true })>;
type ItemSummary = z.infer<typeof ItemSchema.pick({ id: true, name: true })>;Using inferred types in function signatures:
const ApiResponse = z.object({
data: z.array(UserSchema),
total: z.number(),
page: z.number(),
});
type ApiResponse = z.infer<typeof ApiResponse>;
async function fetchUsers(page: number): Promise<ApiResponse> {
const res = await fetch(`/api/users?page=${page}`);
return ApiResponse.parse(await res.json());
}Generic schema-driven components:
function SchemaForm<T extends z.ZodObject<any>>({
schema,
onSubmit,
}: {
schema: T;
onSubmit: (data: z.infer<T>) => void;
}) {
// Build form fields from schema.shape
const fields = Object.keys(schema.shape);
// ...
}// z.infer is a type alias for z.output
type Infer<T extends z.ZodType> = T["_output"];
type Input<T extends z.ZodType> = T["_input"];
type Output<T extends z.ZodType> = T["_output"];
// Defaults make fields optional in input but required in output
const S = z.object({ count: z.number().default(0) });
type SIn = z.input<typeof S>; // { count?: number | undefined }
type SOut = z.output<typeof S>; // { count: number }
// Use satisfies with inferred types for type-safe constants
const defaultUser = {
name: "Guest",
email: "guest@example.com",
role: "viewer" as const,
} satisfies Partial<User>;z.infer is output, not input - If your schema has transforms, z.infer gives you the post-transform type. Your form state and API request body need z.input. Fix: Use z.input for form data and request shapes, z.infer (or z.output) for validated results.
Defaults are invisible to z.infer - z.string().default("hi") infers as string in z.infer, not string | undefined. But z.input correctly shows it as string | undefined. Fix: Be aware of the asymmetry when constructing input objects.
Cannot use z.infer without typeof - z.infer<UserSchema> is wrong. Fix: Always write z.infer<typeof UserSchema>.
Circular references need explicit annotation - Recursive schemas with z.lazy cannot be inferred automatically. Fix: Declare the type manually and annotate the schema: const S: z.ZodType<MyType> = z.lazy(...).
| Alternative | Use When | Don't Use When |
|---|---|---|
| Manual TypeScript interfaces | You have no runtime validation needs | You want a single source of truth for types and validation |
| TypeBox | You need JSON Schema output alongside TypeScript types | You do not need JSON Schema interop |
Valibot InferOutput | You use Valibot and want the same pattern | You are standardized on Zod |
| tRPC inferring from routers | Your types flow through tRPC end-to-end | You need standalone validation outside tRPC |
z.infer is an alias for z.output -- the type after transforms and defaultsz.input is the type before transforms and defaults -- what you pass into parse()Use z.input for form state, request bodies, and any code that constructs the raw data before parsing. Use z.infer (output) for the validated result after parsing.
const Item = z.object({ id: z.string(), name: z.string(), price: z.number() });
type CreateItem = z.infer<typeof Item.omit({ id: true })>;
type UpdateItem = z.infer<typeof Item.partial().required({ id: true })>;
type ItemSummary = z.infer<typeof Item.pick({ id: true, name: true })>;const S = z.object({ count: z.number().default(0) });
type SIn = z.input<typeof S>; // { count?: number | undefined }
type SOut = z.output<typeof S>; // { count: number }The field is optional in input but guaranteed in output.
const S = z.string().transform((s) => new Date(s));
type SIn = z.input<typeof S>; // string
type SOut = z.output<typeof S>; // Dateconst ApiResponse = z.object({ data: z.array(UserSchema), total: z.number() });
type ApiResponse = z.infer<typeof ApiResponse>;
async function fetchUsers(): Promise<ApiResponse> {
const res = await fetch("/api/users");
return ApiResponse.parse(await res.json());
}.pick(), .omit(), .extend(), .merge(), .partial(), .required(), and .deepPartial() all return new schemas whose z.infer types update automatically.
You must use typeof: z.infer<typeof UserSchema>. The generic parameter expects the type of the schema variable, not the value itself.
TypeScript cannot infer recursive types from z.lazy. You must declare the type manually and annotate the schema:
type Category = { name: string; children: Category[] };
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({ name: z.string(), children: z.array(CategorySchema) })
);function SchemaForm<T extends z.ZodObject<any>>({
schema,
onSubmit,
}: {
schema: T;
onSubmit: (data: z.infer<T>) => void;
}) {
const fields = Object.keys(schema.shape);
// ...
}z.infer<T> resolves to T["_output"]z.input<T> resolves to T["_input"]ZodType instanceconst defaultUser = {
name: "Guest",
email: "guest@example.com",
role: "viewer" as const,
} satisfies Partial<User>;This checks the object matches the type without widening it.
Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥