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 be undefined or not present
  • .nullable() = may be null (but must be present)
  • .nullish() = may be undefined or null

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

  1. Use z.coerce for HTML form inputs
  2. Prefer z.email() over z.string().email()
  3. Use safeParse unless you’re sure your data is valid
  4. Reuse schemas to keep definitions consistent
  5. 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

Start simple and unlock more advanced features whenever you need them!