React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

zodinferinputoutputtypescripttypes

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.infer for the validated output type
  • z.input for 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 that parse() returns (the output type)
  • z.input<typeof Schema> extracts the type that parse() accepts (before transforms and defaults)
  • When a schema has no transforms or defaults, z.input and z.output are 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.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.inferz.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 typeofz.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(...).

Alternatives

AlternativeUse WhenDon't Use When
Manual TypeScript interfacesYou have no runtime validation needsYou want a single source of truth for types and validation
TypeBoxYou need JSON Schema output alongside TypeScript typesYou do not need JSON Schema interop
Valibot InferOutputYou use Valibot and want the same patternYou are standardized on Zod
tRPC inferring from routersYour types flow through tRPC end-to-endYou need standalone validation outside tRPC

FAQs

What is the difference between z.infer, z.input, and z.output?
  • z.infer is an alias for z.output -- the type after transforms and defaults
  • z.input is the type before transforms and defaults -- what you pass into parse()
  • 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>;  // Date
How 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 to T["_output"]
  • z.input<T> resolves to T["_input"]
  • These are branded properties on every ZodType instance
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.