Artigo
Zod 4: The Ultimate Practical Guide to Fast TypeScript Validation
Master data validation in TypeScript with Zod 4. Learn coerce, enums, safeParse, React Hook Form integration, and lightning-fast new features.
Zod is a schema validation library that auto-generates TypeScript types. With Zod 4, it’s now 14x faster and even more powerful. Here’s everything you need for using Zod in your day-to-day coding.
Installation
npm install zod@^4.0.0
Basic Concepts
Primitive Types
import { z } from 'zod';
// Basic types
z.string();
z.number();
z.boolean();
z.date();
// Using them
const name = z.string();
name.parse("João"); // ✅ "João"
name.parse(123); // ❌ Throws error!
Objects
const userSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().min(18)
});
// Validating
const user = userSchema.parse({
name: "Maria",
email: "maria@email.com",
age: 25
}); // ✅ Works!
// Auto-generate type
type User = z.infer<typeof userSchema>;
// { name: string; email: string; age: number }
Coerce - Automatic Type Conversion
The Problem: HTML forms always return strings, but you need numbers.
// ❌ Breaks - HTML input returns "25" (string)
const badSchema = z.object({
age: z.number()
});
// ✅ Works - automatically converts "25" to 25
const goodSchema = z.object({
age: z.coerce.number()
});
// More useful examples
z.coerce.number() // "123" → 123
z.coerce.boolean() // "true" → true
z.coerce.date() // "2024-01-01" → new Date()
String Validations
// Basic checks
z.string().min(3, "Too short")
z.string().max(100, "Too long")
z.string().email("Invalid email")
z.string().url("Invalid URL")
// Automatic formatting
z.string().trim() // Trims spaces
z.string().toLowerCase() // Lowercase
z.string().toUpperCase() // Uppercase
// Full example
const passwordSchema = z.string()
.min(8, "At least 8 characters")
.regex(/[A-Z]/, "Must contain uppercase letter")
.regex(/[0-9]/, "Must contain a number");
Number Validations
z.number().positive() // > 0
z.number().min(0) // >= 0
z.number().max(100) // <= 100
z.number().int() // Integer only
z.number().multipleOf(5) // Multiple of 5
// Practical example
const priceSchema = z.coerce.number()
.positive("Price must be positive")
.max(10000, "Price too high");
Enum - Restricting Values
// Limited options
def statusSchema = z.enum(["pending", "approved", "rejected"]);
// In objects
const orderSchema = z.object({
id: z.string(),
status: z.enum(["pending", "shipped", "delivered"]),
priority: z.enum(["low", "medium", "high"])
});
Arrays
// Array of strings
z.array(z.string())
// Array of objects
z.array(z.object({
name: z.string(),
age: z.number()
}))
// With validations
z.array(z.string())
.min(1, "At least 1 item required")
.max(10, "Maximum of 10 items allowed")
Optional vs Nullable vs Nullish
The difference that trips people up!
const schema = z.object({
// OPTIONAL: might not exist
age: z.number().optional(),
// NULLABLE: can be null (but must exist)
avatar: z.string().nullable(),
// NULLISH: can be undefined OR null
bio: z.string().nullish()
});
// Valid examples:
schema.parse({
avatar: null, // ✅ nullable accepts null
bio: undefined // ✅ nullish accepts undefined
// age does not have to exist
});
Quick Reference:
.optional()= may beundefinedor not present.nullable()= may benull(but must be present).nullish()= may beundefinedornull
Forms with React Hook Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
age: z.coerce.number().min(18, "Must be 18+"),
type: z.enum(["user", "admin"])
});
type FormData = z.infer<typeof formSchema>;
function MyForm() {
const form = useForm<FormData>({
resolver: zodResolver(formSchema)
});
const onSubmit = (data: FormData) => {
console.log(data); // Data is already validated!
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* your fields here */}
</form>
);
}
Custom Validations
// Simple validation
const passwordSchema = z.string()
.refine(
(password) => password.includes("@") || password.includes("#"),
{ message: "Must include @ or #" }
);
// Cross-field validation
const signupSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string()
})
.refine(
(data) => data.password === data.confirmPassword,
{ message: "Passwords do not match", path: ["confirmPassword"] }
);
Transformations
// Clean and format data
const userSchema = z.object({
name: z.string().trim().toLowerCase(),
email: z.string().email().toLowerCase(),
age: z.coerce.number()
});
// Input: { name: " JOÃO ", email: "JOAO@EMAIL.COM", age: "25" }
// Output: { name: "joão", email: "joao@email.com", age: 25 }
Default Values
// Simple default value
z.string().default("not specified")
// Default value from function
z.date().default(() => new Date())
// Practical example
const configSchema = z.object({
theme: z.enum(["light", "dark"]).default("light"),
notifications: z.boolean().default(true)
});
Parse vs SafeParse
// parse() - throws on invalid
try {
const result = schema.parse(data);
console.log(result);
} catch (error) {
console.log(error.errors);
}
// safeParse() - returns success/error object
const result = schema.safeParse(data);
if (result.success) {
console.log(result.data);
} else {
console.log(result.error.errors);
}
Reusing Schemas
// Base schema
const addressSchema = z.object({
street: z.string(),
city: z.string(),
zipCode: z.string()
});
// Reusing
const userSchema = z.object({
name: z.string(),
homeAddress: addressSchema,
workAddress: addressSchema.optional()
});
What’s New in Zod 4
Top-Level String Formats
// Now you can use these directly (cleaner)
z.email()
z.url()
z.uuid()
z.ipv4()
z.ipv6()
// Instead of (still works, but deprecated)
z.string().email()
z.string().url()
Template Literals
// Validate custom patterns
const cssUnit = z.templateLiteral([z.number(), z.enum(["px", "em", "rem"])]);
// Accepts: "10px", "1.5em", "2rem"
const greeting = z.templateLiteral(["hello, ", z.string()]);
// Accepts: "hello, world", "hello, João"
Stringbool - For Environment Variables
const envSchema = z.object({
DEBUG: z.stringbool(), // converts "true"/"false" to boolean
PORT: z.coerce.number()
});
// Accepts: "true", "1", "yes", "on" → true
// Accepts: "false", "0", "no", "off" → false
Helpful Object Methods
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
password: z.string()
});
// Pick certain fields only
const publicUser = userSchema.pick({
id: true,
name: true,
email: true
});
// Omit fields
const userWithoutPassword = userSchema.omit({
password: true
});
// Make all fields optional
const partialUser = userSchema.partial();
Key Tips
- Use
z.coercefor HTML form inputs - Prefer
z.email()overz.string().email() - Use
safeParseunless you’re sure your data is valid - Reuse schemas to keep definitions consistent
- Integrate with React Hook Form for seamless forms
Conclusion
Zod 4 is a massive leap forward that makes data validation in TypeScript dramatically simpler and faster. With 14x performance and cleaner APIs, Zod is the top choice for any TypeScript project.
Key benefits:
- ✅ Automatic type safety
- ✅ Coerce for raw form inputs
- ✅ Much better performance
- ✅ Clean and intuitive API
- ✅ Flawless integration with React Hook Form