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.
Recipe
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.
Working Example
"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:
- Single schema as source of truth for multiple derived types
z.inferfor the validated output typez.inputfor the raw input type (before defaults and transforms)- Schema derivation with
.omit(),.partial(),.required()
Deep Dive
How It Works
z.infer<typeof Schema>extracts the TypeScript type thatparse()returns (the output type)z.input<typeof Schema>extracts the type thatparse()accepts (before transforms and defaults)- When a schema has no transforms or defaults,
z.inputandz.outputare identical - Transforms, defaults,
.optional(),.nullable()can all cause input and output to differ - Schema derivation methods (
.pick(),.omit(),.extend(),.partial()) produce new schemas with correctly inferred types
Variations
Deriving 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);
// ...
}TypeScript Notes
// 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>;Gotchas
-
z.inferis output, not input — If your schema has transforms,z.infergives you the post-transform type. Your form state and API request body needz.input. Fix: Usez.inputfor form data and request shapes,z.infer(orz.output) for validated results. -
Defaults are invisible to
z.infer—z.string().default("hi")infers asstringinz.infer, notstring | undefined. Butz.inputcorrectly shows it asstring | undefined. Fix: Be aware of the asymmetry when constructing input objects. -
Cannot use
z.inferwithouttypeof—z.infer<UserSchema>is wrong. Fix: Always writez.infer<typeof UserSchema>. -
Circular references need explicit annotation — Recursive schemas with
z.lazycannot be inferred automatically. Fix: Declare the type manually and annotate the schema:const S: z.ZodType<MyType> = z.lazy(...).
Alternatives
| 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 |
FAQs
What is the difference between z.infer, z.input, and z.output?
z.inferis an alias forz.output-- the type after transforms and defaultsz.inputis the type before transforms and defaults -- what you pass intoparse()- When a schema has no transforms or defaults, all three are identical
When should you use z.input instead of z.infer?
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.
How do you derive CRUD types from a single base schema?
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 })>;How does .default() affect z.input vs z.infer?
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.
How do transforms cause z.input and z.output to differ?
const S = z.string().transform((s) => new Date(s));
type SIn = z.input<typeof S>; // string
type SOut = z.output<typeof S>; // DateHow do you use an inferred type in a function signature?
const 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());
}What schema methods produce new schemas with correctly inferred types?
.pick(), .omit(), .extend(), .merge(), .partial(), .required(), and .deepPartial() all return new schemas whose z.infer types update automatically.
Gotcha: Why does z.infer with a schema value instead of its type produce a TypeScript error?
You must use typeof: z.infer<typeof UserSchema>. The generic parameter expects the type of the schema variable, not the value itself.
Gotcha: Why can't z.infer resolve a recursive schema made with z.lazy?
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) })
);How do you build a generic component that accepts any Zod object schema?
function SchemaForm<T extends z.ZodObject<any>>({
schema,
onSubmit,
}: {
schema: T;
onSubmit: (data: z.infer<T>) => void;
}) {
const fields = Object.keys(schema.shape);
// ...
}TypeScript: What are the internal type accessors behind z.infer and z.input?
z.infer<T>resolves toT["_output"]z.input<T>resolves toT["_input"]- These are branded properties on every
ZodTypeinstance
TypeScript: How do you use satisfies with an inferred type?
const defaultUser = {
name: "Guest",
email: "guest@example.com",
role: "viewer" as const,
} satisfies Partial<User>;This checks the object matches the type without widening it.
Related
- Zod Basics — schema definition fundamentals
- Zod Types — all available types
- Zod Transforms — transforms that affect input/output types
- RHF + Zod — typed forms from inferred schemas