Zod is a framework that covers a gap that happens because typescript gets compiled into javascript. Typescript being strongly typed, but java not, you are covered are compile-time for type checks, but that is lost at run time. Zod is here to do perform type checks at runtime plus adding data validations and even transformation.
The Core Difference: Compile-Time vs Runtime
TypeScript Types = Compile-Time Only
// This type disappears after compilation interface User { name: string; age: number; } // TypeScript thinks this is fine at compile time const userData: User = await fetch('/api/user').then(r => r.json()); console.log(userData.name); // 💥 Runtime error if API returns { username: "John" }
Zod = Runtime Validation + Type Inference
import { z } from 'zod'; // This exists at runtime AND generates TypeScript types const UserSchema = z.object({ name: z.string(), age: z.number() }); // This will throw at runtime if data doesn't match const userData = UserSchema.parse(await fetch('/api/user').then(r => r.json())); console.log(userData.name); // ✅ Guaranteed to exist and be a string
What Zod Adds: The Advantages
1. Runtime Safety for External Data
// ❌ TypeScript alone - false confidence interface APIResponse { users: Array<{ id: number; email: string }>; } // TypeScript: "Looks good!" // Reality: API might return { users: null } or { data: [...] } const response: APIResponse = await fetchAPI(); response.users.map(...); // 💥 Cannot read property 'map' of null // ✅ With Zod - actual validation const APIResponseSchema = z.object({ users: z.array(z.object({ id: z.number(), email: z.string().email() // Even validates email format! })) }); try { const response = APIResponseSchema.parse(await fetchAPI()); response.users.map(...); // ✅ Guaranteed safe } catch (error) { // Handle malformed response }
2. Single Source of Truth
// ❌ TypeScript - duplicate definitions interface User { email: string; age: number; } function validateUser(data: any): data is User { return typeof data.email === 'string' && typeof data.age === 'number' && data.age >= 0; // Oops, forgot this in the type! } // ✅ Zod - one definition, both type and validation const UserSchema = z.object({ email: z.string().email(), age: z.number().min(0) }); type User = z.infer<typeof UserSchema>; // Type derived from schema const isValid = UserSchema.safeParse(data).success; // Validation from same source
3. Rich Validation Beyond Types
// ❌ TypeScript can't express these constraints interface Password { value: string; // Can't say "min 8 chars, must have uppercase" } // ✅ Zod can validate complex rules const PasswordSchema = z.string() .min(8, "Password must be at least 8 characters") .regex(/[A-Z]/, "Must contain uppercase letter") .regex(/[0-9]/, "Must contain number") .regex(/[^A-Za-z0-9]/, "Must contain special character");
4. Transformation and Coercion
// ✅ Zod can transform data while validating const ConfigSchema = z.object({ port: z.string().transform(Number), // "3000" → 3000 enabled: z.string().transform(s => s === "true"), // "true" → true createdAt: z.string().pipe(z.coerce.date()), // "2024-01-01" → Date object email: z.string().toLowerCase().trim().email() // Clean and validate }); // URL params, form data, environment variables - all strings! const config = ConfigSchema.parse({ port: "3000", enabled: "true", createdAt: "2024-01-01", email: " USER@EXAMPLE.COM " }); // Result: { port: 3000, enabled: true, createdAt: Date, email: "user@example.com" }
5. Better Error Messages
// ❌ TypeScript at runtime JSON.parse(apiResponse); // Error: Unexpected token < in JSON at position 0 // ✅ Zod validation errors UserSchema.parse(data); /* ZodError: { "issues": [{ "path": ["email"], "message": "Invalid email format" }, { "path": ["age"], "message": "Expected number, received string" }] } */
6. Composability
// Build complex schemas from simple ones const AddressSchema = z.object({ street: z.string(), city: z.string(), zip: z.string().regex(/^\d{5}$/) }); const PersonSchema = z.object({ name: z.string(), addresses: z.array(AddressSchema), // Reuse schemas primaryAddress: AddressSchema.optional() }); const CompanySchema = z.object({ employees: z.array(PersonSchema), // Compose further headquarters: AddressSchema });
Real-World Example: Your HeadingAnalyzer
// What could go wrong without runtime validation? // 1. DOM parsing might produce unexpected results const heading = { level: 7, // ❌ TypeScript won't catch this at runtime text: null, // ❌ Might be null instead of empty string position: -1, // ❌ Could be negative id: undefined // ❌ Might be undefined instead of null }; // 2. Cloudflare Worker receives malformed data const apiResponse = await fetch('/analyze'); const data: HeadingAnalysisResult = await apiResponse.json(); // TypeScript: "Looks good!" // Reality: Could be { error: "Rate limited" } or literally anything // 3. With Zod, you catch these issues immediately const HeadingInfoSchema = z.object({ level: z.number().min(1).max(6), // Must be 1-6 text: z.string(), // Coerces null to empty string position: z.number().int().nonnegative(), // Must be positive integer id: z.string().nullable() // Explicitly nullable, not undefined });
When You NEED Zod
- API Boundaries: Validating external API responses
- User Input: Form data, URL params, file uploads
- Config Files: Environment variables, JSON configs
- Database Results: Ensuring DB schema matches expectations
- Webhooks: Third-party services sending data
- localStorage/sessionStorage: Stored data might be corrupted
When TypeScript Types Are Enough
- Internal Code: Functions calling other functions you control
- Build-Time Known: Imported JSON, constants
- Trusted Sources: Your own internal services with guaranteed contracts
- Performance Critical: Hot paths where validation overhead matters
In conclusion, TypeScript protects you from yourself (typos, wrong types in your code). Zod protects you from the world (APIs, users, databases, external systems).
Top comments (1)
I discovered Zod as Astro recommends or expects it when using MDX front matter content collections with TypeScript, and I found it very useful. Despite the "single source of truth", I still have more than a single place to edit my type definitions, e.g.
and
I wondered if there is an elegant way to avoid this data duplication. Update: I eventually found out about type inference like this ...
... but still struggled to make it work.
Infer Astro Zod Content Schema in TSX React Components avoiding Code Duplication
Ingo Steinke, web developer ・ Aug 28