Zod v4 schema definition patterns with the Type Inference Chain for absolute type safety. PROACTIVELY activate for: (1) defining Zod schemas as single source of truth, (2) using z.infer for type generation, (3) implementing safe parsing with safeParse. Triggers: "zod", "schema", "z.infer"
Install
npx skillscat add agentient/vibekit/zod-schema-type-inference-chain Install via the SkillsCat registry.
Zod Schema Type Inference Chain
The Type Inference Chain Pattern (CRITICAL)
The Type Inference Chain is the foundational pattern for all state and form management:
Zod Schema (single source of truth)
|
z.infer<typeof schema> (generate TypeScript type)
|
useForm<Type>() (type-safe form)
|
Zustand Store<Type> (type-safe state)This pattern ensures absolute type safety across your entire data layer by maintaining one single source of truth: the Zod schema.
Why This Pattern is Critical
Problem Without Type Inference Chain:
// BAD: Multiple sources of truth
interface UserData { // Manual type definition
email: string;
age: number;
}
const userSchema = z.object({ // Zod schema
email: z.string().email(),
age: z.number().min(18),
});
// These can drift out of sync!Solution With Type Inference Chain:
// GOOD: Single source of truth
const userSchema = z.object({
email: z.string().email('Invalid email'),
age: z.number().min(18, 'Must be 18+'),
});
// Type is automatically inferred from schema
type UserData = z.infer<typeof userSchema>;
// Now the schema and type are ALWAYS in sync!Schema Definition Patterns
Basic Object Schema
import { z } from 'zod';
const userSchema = z.object({
// String with validations
username: z.string()
.min(3, 'Username must be at least 3 characters')
.max(20, 'Username must be at most 20 characters')
.regex(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores'),
// Email validation
email: z.string().email('Invalid email address'),
// Number with range
age: z.number()
.int('Must be a whole number')
.min(18, 'Must be 18 or older')
.max(120, 'Invalid age'),
// Optional field
middleName: z.string().optional(),
// Nullable field
nickname: z.string().nullable(),
// Enum
role: z.enum(['user', 'admin', 'moderator']),
// Boolean
isActive: z.boolean(),
// Array
tags: z.array(z.string())
.min(1, 'At least one tag required')
.max(5, 'Maximum 5 tags'),
});
// Infer TypeScript type
type User = z.infer<typeof userSchema>;Nested Object Schema
const addressSchema = z.object({
street: z.string().min(1, 'Street required'),
city: z.string().min(1, 'City required'),
state: z.string().length(2, 'Use 2-letter state code'),
zipCode: z.string().regex(/^\d{5}$/, 'Must be 5 digits'),
});
const userWithAddressSchema = z.object({
name: z.string(),
email: z.string().email(),
address: addressSchema, // Nested object
});
type UserWithAddress = z.infer<typeof userWithAddressSchema>;Schema Composition and Reusability
Using .extend()
const baseUserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
});
// Add more fields to base schema
const fullUserSchema = baseUserSchema.extend({
name: z.string(),
age: z.number(),
role: z.enum(['user', 'admin']),
});
type FullUser = z.infer<typeof fullUserSchema>;Using .pick() and .omit()
const userSchema = z.object({
id: z.string(),
email: z.string().email(),
password: z.string(),
name: z.string(),
});
// Pick only specific fields
const loginSchema = userSchema.pick({
email: true,
password: true,
});
// { email: string; password: string; }
// Omit specific fields
const publicUserSchema = userSchema.omit({
password: true,
});
// { id: string; email: string; name: string; }Cross-Field Validation with .refine()
Password Confirmation
const passwordSchema = z.object({
password: z.string()
.min(8, 'Password must be at least 8 characters'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'], // Error appears on confirmPassword field
});
type PasswordForm = z.infer<typeof passwordSchema>;Conditional Validation
const shippingSchema = z.object({
shippingMethod: z.enum(['pickup', 'delivery']),
address: z.string().optional(),
}).refine(
(data) => {
// If delivery is selected, address is required
if (data.shippingMethod === 'delivery') {
return data.address && data.address.length > 0;
}
return true;
},
{
message: 'Address required for delivery',
path: ['address'],
}
);Safe Error Handling with .safeParse()
Basic Safe Parsing
const userSchema = z.object({
email: z.string().email(),
age: z.number().min(18),
});
// Unsafe data from user input or API
const userData = {
email: 'invalid-email',
age: 15,
};
// Use safeParse for validation
const result = userSchema.safeParse(userData);
if (!result.success) {
// Validation failed - handle errors
const formatted = result.error.format();
console.log(formatted.email?._errors); // ["Invalid email address"]
console.log(formatted.age?._errors); // ["Must be 18 or older"]
// Get flat errors
const flat = result.error.flatten();
console.log(flat.fieldErrors);
// {
// email: ["Invalid email address"],
// age: ["Must be 18 or older"]
// }
} else {
// Validation succeeded - use validated data
const validUser = result.data; // Fully typed!
}Anti-Patterns (DO NOT DO)
Manually Defining Types
// WRONG: Manual type separate from schema
interface UserData {
email: string;
age: number;
}
const userSchema = z.object({
email: z.string().email(),
age: z.number(),
});
// Problem: These can drift out of sync!Correct:
const userSchema = z.object({
email: z.string().email(),
age: z.number(),
});
type UserData = z.infer<typeof userSchema>; // Always in sync!Using .parse() Without Try/Catch
// WRONG: Throws unhandled exception on invalid data
const user = userSchema.parse(untrustedData);Correct:
const result = userSchema.safeParse(untrustedData);
if (!result.success) {
// Handle error
} else {
const user = result.data;
}Summary
The zod-schema-type-inference-chain skill establishes the foundational pattern:
- Define Zod schema - Single source of truth for data structure and validation
- Infer TypeScript type - Use
z.infer<typeof schema>to generate type - Use in forms - Pass type to
useForm<Type>({ resolver: zodResolver(schema) }) - Use in stores - Pass type to Zustand store interface
- Validate safely - Use
.safeParse()for all external data
Related Skills: rhf-zod-schema-integration, zustand-v5-typed-store-creation