Documentation Index
Fetch the complete documentation index at: https://mintlify.com/colinhacks/zod/llms.txt
Use this file to discover all available pages before exploring further.
Overview
One of Zod’s most powerful features is automatic type inference. You define validation logic once, and TypeScript types are automatically generated.
Basic Type Inference with z.infer
Use z.infer<typeof schema> to extract the TypeScript type:
import { z } from 'zod';
const User = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
age: z.number().optional(),
});
// Extract type
type User = z.infer<typeof User>;
// Equivalent to:
// type User = {
// id: number;
// name: string;
// email: string;
// age?: number;
// }
// Use the type
const user: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
};
z.infer is actually an alias for z.output. They are identical and interchangeable.
Primitive Types
const str = z.string();
type Str = z.infer<typeof str>; // string
const num = z.number();
type Num = z.infer<typeof num>; // number
const bool = z.boolean();
type Bool = z.infer<typeof bool>; // boolean
const bigInt = z.bigint();
type BigInt = z.infer<typeof bigInt>; // bigint
const date = z.date();
type DateType = z.infer<typeof date>; // Date
const undef = z.undefined();
type Undef = z.infer<typeof undef>; // undefined
const nul = z.null();
type Nul = z.infer<typeof nul>; // null
Complex Types
Arrays
const stringArray = z.array(z.string());
type StringArray = z.infer<typeof stringArray>; // string[]
const userArray = z.array(User);
type UserArray = z.infer<typeof userArray>; // User[]
Objects
const Person = z.object({
firstName: z.string(),
lastName: z.string(),
age: z.number(),
address: z.object({
street: z.string(),
city: z.string(),
country: z.string(),
}),
});
type Person = z.infer<typeof Person>;
// {
// firstName: string;
// lastName: string;
// age: number;
// address: {
// street: string;
// city: string;
// country: string;
// };
// }
Unions
const StringOrNumber = z.union([z.string(), z.number()]);
type StringOrNumber = z.infer<typeof StringOrNumber>; // string | number
const Result = z.union([
z.object({ success: z.literal(true), data: z.string() }),
z.object({ success: z.literal(false), error: z.string() }),
]);
type Result = z.infer<typeof Result>;
// { success: true; data: string } | { success: false; error: string }
Enums
const Fruit = z.enum(['apple', 'banana', 'orange']);
type Fruit = z.infer<typeof Fruit>; // 'apple' | 'banana' | 'orange'
const Status = z.enum(['pending', 'active', 'inactive']);
type Status = z.infer<typeof Status>; // 'pending' | 'active' | 'inactive'
Optional and Nullable Types
const optionalString = z.string().optional();
type OptionalString = z.infer<typeof optionalString>; // string | undefined
const nullableString = z.string().nullable();
type NullableString = z.infer<typeof nullableString>; // string | null
const nullishString = z.string().nullish();
type NullishString = z.infer<typeof nullishString>; // string | null | undefined
// In objects
const Schema = z.object({
required: z.string(),
optional: z.string().optional(),
nullable: z.string().nullable(),
});
type Schema = z.infer<typeof Schema>;
// {
// required: string;
// optional?: string | undefined;
// nullable: string | null;
// }
When schemas include transformations, the input type (before transformation) differs from the output type (after transformation).
const schema = z.string().transform((val) => val.length);
type Input = z.input<typeof schema>; // string
type Output = z.output<typeof schema>; // number
From packages/zod/src/v4/classic/tests/transform.test.ts:170-174:
const stringToNumber = z.string().transform((arg) => Number.parseFloat(arg));
const t1 = z.object({ stringToNumber });
type Input = z.input<typeof t1>; // { stringToNumber: string }
type Output = z.output<typeof t1>; // { stringToNumber: number }
When They Differ
const NumberFromString = z.string().transform(Number);
type Input = z.input<typeof NumberFromString>; // string
type Output = z.output<typeof NumberFromString>; // number
const result = NumberFromString.parse('42'); // result is number
const WithDefault = z.string().default('hello');
type Input = z.input<typeof WithDefault>; // string | undefined
type Output = z.output<typeof WithDefault>; // string
// Input can be undefined, output is always string
const result = WithDefault.parse(undefined); // 'hello'
const Preprocessed = z.preprocess(
(val) => String(val),
z.string()
);
type Input = z.input<typeof Preprocessed>; // unknown
type Output = z.output<typeof Preprocessed>; // string
const Piped = z.string().pipe(z.number());
type Input = z.input<typeof Piped>; // string
type Output = z.output<typeof Piped>; // number
const FormSchema = z.object({
name: z.string(),
age: z.string().transform(Number),
subscribe: z.string().transform((val) => val === 'true'),
});
type FormInput = z.input<typeof FormSchema>;
// {
// name: string;
// age: string;
// subscribe: string;
// }
type FormOutput = z.output<typeof FormSchema>;
// {
// name: string;
// age: number;
// subscribe: boolean;
// }
// Usage
const formData = new FormData();
const input: FormInput = {
name: formData.get('name') as string,
age: formData.get('age') as string,
subscribe: formData.get('subscribe') as string,
};
const output: FormOutput = FormSchema.parse(input);
// output.age is a number
// output.subscribe is a boolean
Type Narrowing with Refinements
Refinements can narrow types using type predicates:
const isValidEmail = (val: string): val is `${string}@${string}` => {
return val.includes('@');
};
const schema = z.string().refine(isValidEmail);
type Input = z.input<typeof schema>; // string
type Output = z.output<typeof schema>; // `${string}@${string}`
From packages/zod/src/v4/classic/tests/refine.test.ts:423-424:
const schema = z.string().refine((val): val is 'a' => val === 'a');
type Input = z.input<typeof schema>; // string
type Output = z.output<typeof schema>; // 'a'
Recursive Types
Zod supports recursive type inference:
const Category: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(Category),
})
);
type Category = z.infer<typeof Category>;
// type Category = {
// name: string;
// subcategories: Category[];
// }
From packages/zod/src/v4/classic/tests/recursive-types.test.ts:28-31:
const Category: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(Category),
})
);
type Category = z.infer<typeof Category>;
Utility Type Helpers
TypeOf (Alternative to infer)
import { z } from 'zod';
const User = z.object({
name: z.string(),
age: z.number(),
});
// Both are equivalent
type User1 = z.infer<typeof User>;
type User2 = z.TypeOf<typeof User>;
const User = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
});
// Get keys as type
type UserKeys = keyof z.infer<typeof User>; // 'id' | 'name' | 'email'
// Pick specific fields
type UserNameEmail = Pick<z.infer<typeof User>, 'name' | 'email'>;
// { name: string; email: string }
Common Patterns
Sharing Types Between Schema and Interface
// Define schema first
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
createdAt: z.date(),
});
// Infer type from schema
type User = z.infer<typeof UserSchema>;
// Use in function signatures
function createUser(data: z.input<typeof UserSchema>): User {
return UserSchema.parse(data);
}
function updateUser(id: number, data: Partial<User>): User {
// ...
}
API Request/Response Types
const CreateUserRequest = z.object({
name: z.string(),
email: z.string().email(),
password: z.string().min(8),
});
const UserResponse = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
createdAt: z.string().datetime(),
});
type CreateUserRequest = z.infer<typeof CreateUserRequest>;
type UserResponse = z.infer<typeof UserResponse>;
// Use in API handler
async function createUser(
req: CreateUserRequest
): Promise<UserResponse> {
// Implementation
}
Generic Type Utilities
// Extract array element type
type ArrayElement<T> = T extends z.ZodArray<infer U> ? z.infer<U> : never;
const users = z.array(UserSchema);
type User = ArrayElement<typeof users>; // Inferred User type
// Extract unwrapped optional type
type Unwrap<T> = T extends z.ZodOptional<infer U> ? z.infer<U> : z.infer<T>;
const optional = z.string().optional();
type Unwrapped = Unwrap<typeof optional>; // string
Edge Cases and Special Types
Literal Types
const literal = z.literal('hello');
type Literal = z.infer<typeof literal>; // 'hello'
const numLiteral = z.literal(42);
type NumLiteral = z.infer<typeof numLiteral>; // 42
const boolLiteral = z.literal(true);
type BoolLiteral = z.infer<typeof boolLiteral>; // true
Template Literals
const email = z.templateLiteral([
z.string(),
z.literal('@'),
z.string(),
]);
type Email = z.infer<typeof email>; // `${string}@${string}`
Never Type
const never = z.never();
type Never = z.infer<typeof never>; // never
Unknown and Any
const unknown = z.unknown();
type Unknown = z.infer<typeof unknown>; // unknown
const any = z.any();
type Any = z.infer<typeof any>; // any
Best Practices
- Define schema once, infer types - Don’t duplicate type definitions
- Use z.infer for most cases - It’s an alias for
z.output
- Use z.input for transformations - When input differs from output
- Name types same as schema - Makes code more readable
- Export both schema and type - Useful for consumers
// Good pattern
export const UserSchema = z.object({ /* ... */ });
export type User = z.infer<typeof UserSchema>;
// Consumers can use both
import { UserSchema, type User } from './schemas';
TypeScript Integration
Const Assertions
const values = ['a', 'b', 'c'] as const;
const schema = z.enum(values);
type Schema = z.infer<typeof schema>; // 'a' | 'b' | 'c'
Branded Types
const UserId = z.number().brand('UserId');
type UserId = z.infer<typeof UserId>; // number & Brand<'UserId'>
// Type-safe, prevents mixing IDs
function getUser(id: UserId) { /* ... */ }
getUser(123); // Error: number not assignable to UserId
Type inference happens at compile time and has zero runtime cost. However, complex recursive types can slow down TypeScript compilation.
// Fast type inference
const simple = z.object({ name: z.string() });
type Simple = z.infer<typeof simple>;
// Slower type inference (deep recursion)
const complex = z.lazy(() => z.object({
nested: complex.optional(),
}));
type Complex = z.infer<typeof complex>;
Next Steps
- Learn about Transformations to modify data during parsing
- Explore Refinements for custom validation with type narrowing
- Master Schemas to understand all available schema types