Zod 06 🛡️ Inferring Types — z.infer<> and the schema-to-type bridge
A Zod schema is a runtime value and a type declaration at the same time. z.infer<> is the bridge between them: it reads a schema’s TypeScript type and returns it as a type alias. The schema is the source of truth, and the type is derived from it, never written beside it. This is the mechanic that eliminates drift—if the schema changes, the type changes, and the compiler enforces the change everywhere the type is used .
This chapter covers the inference utilities: z.infer<> for the output type, z.input<> and z.output<> for the two sides of a transforming schema, and the cases where the three diverge. It explains why most schemas need only z.infer, why transforms and defaults require the input/output distinction, and how the inferred type is shared across client and server code. The sections that follow walk through z.infer and the input/output split.
Key point: z.infer<typeof Schema> returns the schema’s output type (what parse returns); z.input<typeof Schema> returns the input type (what parse accepts); they differ only when the schema transforms, coerces, or applies a default.
Why inference utilities exist
The hand-written type problem. A schema and a hand-written interface are two declarations of the same shape. When the schema adds a field, the interface does not, and the compiler trusts the interface while the runtime validator trusts the schema. The two drift in the direction that leaves the type claiming more than the schema checks . z.infer removes the second declaration: there is no interface to forget, because the type is the schema .
The transform divergence problem. A schema with .transform() has two types, not one. z.string().transform((s) => new Date(s)) accepts a string and returns a Date . z.infer returns the output type—the Date—because that is what parse returns. But a function that receives raw API data needs the input type—the string—because that is what it is given before parsing. Using z.infer for the input side produces a type error at the call site: the function expects Date, the caller passes string .
The form-handling problem. A form field holds a raw string; the schema coerces it to a number. React Hook Form needs the input type for defaultValues and field registration, and the output type for the submit handler. The three-generic useForm<Input, unknown, Output> exposes both ends, and z.input/z.output supply the types . Skipping the distinction forces casts that hide the mismatch between what the DOM holds and what the handler receives.
a. z.infer and the output type
z.infer<typeof Schema> is the default. It extracts the type that parse returns, which is the type most application code works with: the validated, transformed, defaulted value. For schemas without transforms, z.infer and z.input are identical, and there is no need to think about the distinction .
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
});
// The output type: what parse returns
type User = z.infer<typeof UserSchema>;
// { id: string; name: string; email: string }
// Use it wherever the validated value flows
function displayUser(user: User) {
return `${user.name} (${user.email})`;
}
The type is structurally identical to what a hand-written interface would be, but it is derived. Adding a field to UserSchema adds it to User, and every function that accepts User must handle the new field or the compiler complains .
In Zod v4, z.infer and z.output are functionally equivalent; z.infer remains primarily for v3 compatibility, while z.output pairs naturally with z.input for explicit distinction . Both return the post-transform type.
b. z.input and z.output when the schema transforms
A transforming schema is a pipe with a different type at each end. z.input is the type that goes in; z.output (and z.infer) is the type that comes out . The distinction matters whenever the schema coerces, applies a default, or calls .transform().
const DateSchema = z.string().transform((s) => new Date(s));
// What parse accepts
type DateInput = z.input<typeof DateSchema>; // string
// What parse returns
type DateOutput = z.output<typeof DateSchema>; // Date
// z.infer is the same as z.output
type Inferred = z.infer<typeof DateSchema>; // Date
The practical rule is mechanical: function parameters that receive raw, unparsed data use z.input; function parameters that receive parsed data use z.infer (or z.output). A form submit handler that calls Schema.parse(values) receives the raw values and passes them through the parser; its parameter type is z.input. A downstream function that receives the parsed result uses z.infer .
Defaults create the same divergence. A schema with .default() accepts an object where the key is absent and returns an object where the key is present. The input type has the key optional; the output type has it required . z.infer returns the required form, which is what the application code works with after parsing.
Complete Example Session
// ============================================
// PART 1: SCHEMA WITHOUT TRANSFORMS
// ============================================
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
});
// Input and output are identical
type User = z.infer<typeof UserSchema>;
// { id: string; name: string }
// ============================================
// PART 2: SCHEMA WITH TRANSFORM
// ============================================
const EventSchema = z.object({
id: z.string(),
timestamp: z.string().transform((s) => new Date(s)), // string → Date
});
// What the API sends
type EventInput = z.input<typeof EventSchema>;
// { id: string; timestamp: string }
// What the application uses after parsing
type Event = z.infer<typeof EventSchema>;
// { id: string; timestamp: Date }
// ============================================
// PART 3: USING BOTH TYPES CORRECTLY
// ============================================
function handleApiResponse(raw: EventInput): Event {
return EventSchema.parse(raw); // input → output
}
function formatEvent(event: Event): string {
return `${event.id} at ${event.timestamp.toISOString()}`; // Date method
}
// ============================================
// PART 4: DEFAULT CREATES DIVERGENCE
// ============================================
const SettingsSchema = z.object({
theme: z.enum(["light", "dark"]).default("light"),
});
type SettingsInput = z.input<typeof SettingsSchema>;
// { theme?: "light" | "dark" } (optional on input)
type Settings = z.infer<typeof SettingsSchema>;
// { theme: "light" | "dark" } (required on output)
// ============================================
// PART 5: FORM WITH INPUT/OUTPUT SPLIT
// ============================================
// React Hook Form: raw values are input, submit result is output
// useForm<z.input<typeof FormSchema>, unknown, z.infer<typeof FormSchema>>
The parts covered inference without transforms, the transform divergence, the correct use of each type at the boundary, the default-induced split, and the form pattern that types both ends.
Quick Reference
Inference Utilities
| Utility | Returns | Use When |
|---|---|---|
z.infer<typeof S> | Output type (post-transform) | Working with parsed data |
z.output<typeof S> | Output type (same as z.infer) | Pairing with z.input |
z.input<typeof S> | Input type (pre-transform) | Receiving raw data |
Divergence Triggers
| Schema Feature | Input vs Output |
|---|---|
| No transform | Identical |
.transform() | Different (type changes) |
.default() | Input optional, output required |
z.coerce.number() | Input unknown/string, output number |
Common Patterns
| Context | Type to Use |
|---|---|
| Function receiving API response | z.input<typeof S> |
| Function receiving parsed data | z.infer<typeof S> |
Form defaultValues | z.input<typeof S> |
| Form submit handler result | z.infer<typeof S> |
Best Practices
✅ Do This:
// Infer, never hand-write, the type
type User = z.infer<typeof UserSchema>; // ✅
// Use z.input for raw data boundaries
function handle(raw: z.input<typeof Schema>) { ... } // ✅
// Use z.infer for parsed data
function process(data: z.infer<typeof Schema>) { ... } // ✅
// Export the inferred type next to the schema
export const UserSchema = z.object({ ... });
export type User = z.infer<typeof UserSchema>; // ✅
❌ Don’t Do This:
// Hand-written interface beside the schema
interface User { name: string } // ❌ (drifts)
// z.infer for a raw input parameter
function handle(raw: z.infer<typeof TransformedSchema>) { ... } // ❌ (type error)
// Assuming input and output are always identical
// (they diverge with transforms, defaults, coercion) // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Type error at parse boundary | z.infer used for raw input | Use z.input for parameters |
| Default field typed as required on input | z.infer returns output | Use z.input for form defaults |
| Type drifts from schema | Hand-written interface | z.infer only |
z.infer returns unknown in generic wrapper | v4 regression with unconstrained generics | Index _zod.output or upgrade |
Real-World Examples
1. Basic inference
type User = z.infer<typeof UserSchema>;
2. Input type for API handler
function handle(raw: z.input<typeof Schema>) { return Schema.parse(raw); }
3. Output type for business logic
function process(data: z.infer<typeof Schema>) { ... }
4. Transform divergence
z.string().transform(s => new Date(s)); // input string, output Date
5. Default divergence
z.string().default("x"); // input optional, output required
6. Export inferred type
export type User = z.infer<typeof UserSchema>;
7. Form input type
useForm<z.input<typeof Schema>, unknown, z.infer<typeof Schema>>({ ... })
8. Coercion input type
z.coerce.number(); // input unknown, output number
9. z.output pairing
type Out = z.output<typeof Schema>; // same as z.infer
10. Schema composition before inference
const BaseSchema = z.object({ id: z.string() });
const UserSchema = BaseSchema.extend({ name: z.string() });
type User = z.infer<typeof UserSchema>; // extended type
Visual
Schema-to-Type Bridge
┌─────────────────────────────────────────────────────────┐
│ z.infer<typeof Schema> │
│ │
│ const UserSchema = z.object({ │
│ name: z.string(), │
│ age: z.number(), │
│ }); │
│ │ │
│ ▼ │
│ type User = z.infer<typeof UserSchema> │
│ │ │
│ ▼ │
│ { name: string; age: number } │
│ │
│ The type is read from the schema, never written │
└─────────────────────────────────────────────────────────┘
Input/Output Divergence
┌─────────────────────────────────────────────────────────┐
│ TRANSFORM: z.string().transform(s => new Date(s)) │
│ │
│ INPUT OUTPUT │
│ z.input<> z.infer<> / z.output<> │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ string │────────►│ Date │ │
│ └──────────────┘ parse() └──────────────┘ │
│ │
│ Use z.input for Use z.infer for │
│ raw API data parsed application data │
└─────────────────────────────────────────────────────────┘
Default Divergence
┌─────────────────────────────────────────────────────────┐
│ z.string().default("light") │
│ │
│ INPUT OUTPUT │
│ theme?: string theme: string │
│ (optional) (required) │
│ │
│ Form defaultValues Parsed config │
│ use z.input use z.infer │
└─────────────────────────────────────────────────────────┘
Form Type Flow
┌─────────────────────────────────────────────────────────┐
│ REACT HOOK FORM │
│ │
│ useForm< │
│ z.input<typeof Schema>, ◄── field values │
│ unknown, │
│ z.infer<typeof Schema> ◄── submit result │
│ >() │
│ │
│ DOM holds input type │
│ Handler receives output type │
│ Casting collapses the pipe and hides mismatch │
└─────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Output type | z.infer<typeof S> / z.output<typeof S> |
| Input type | z.input<typeof S> |
| Divergence trigger | .transform(), .default(), coercion |
| No transform | Input and output identical |
| Hand-written type | Never; infer from schema |
| Export pattern | export type X = z.infer<typeof Schema> |
| Form typing | useForm<z.input<>, unknown, z.infer<>> |
Key takeaways:
z.inferis the output type. It returns whatparsereturns, which is the validated, transformed, defaulted value that application code works with .- Transforms split the type into two.
z.inputis whatparseaccepts;z.output(andz.infer) is what it returns. Function parameters that receive raw data usez.input; parameters that receive parsed data usez.infer. - Defaults create the same split. A defaulted field is optional on input and required on output.
z.inferreturns the required form . - Hand-written types drift. A schema and an interface are two declarations of the same shape, and they diverge when one changes.
z.inferremoves the second declaration . - Export the inferred type next to the schema. Consumers depend on the derived type, not their own copies, which keeps client and server aligned .
Remember: z.infer is not a convenience; it is the mechanism that makes the schema the single source of truth. The schema is the definition, the type is the derivation, and the compiler enforces the connection. When the schema transforms, the type splits into input and output, and the code at each boundary must use the correct side: raw data uses z.input, parsed data uses z.infer. When the type and the schema disagree, the bug is the hand-written type, not the schema.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!