DEV Community

websilvercraft
websilvercraft

Posted on

What is Zod and what does it bring over typescript type definitions

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" } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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"); 
Enter fullscreen mode Exit fullscreen mode

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" } 
Enter fullscreen mode Exit fullscreen mode

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" }] } */ 
Enter fullscreen mode Exit fullscreen mode

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 }); 
Enter fullscreen mode Exit fullscreen mode

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 }); 
Enter fullscreen mode Exit fullscreen mode

When You NEED Zod

  1. API Boundaries: Validating external API responses
  2. User Input: Form data, URL params, file uploads
  3. Config Files: Environment variables, JSON configs
  4. Database Results: Ensuring DB schema matches expectations
  5. Webhooks: Third-party services sending data
  6. localStorage/sessionStorage: Stored data might be corrupted

When TypeScript Types Are Enough

  1. Internal Code: Functions calling other functions you control
  2. Build-Time Known: Imported JSON, constants
  3. Trusted Sources: Your own internal services with guaranteed contracts
  4. 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)

Collapse
 
ingosteinke profile image
Ingo Steinke, web developer • Edited

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.

// content.config.ts (Zod) icon: z.enum(['book', 'blogpost']).default('book').optional(), 
Enter fullscreen mode Exit fullscreen mode

and

// Book.tsx (TypeScript JSX) interface CardProps { icon?: 'book'|'blogpost'; 
Enter fullscreen mode Exit fullscreen mode

I wondered if there is an elegant way to avoid this data duplication. Update: I eventually found out about type inference like this ...

// Book.tsx import type { z } from 'zod'; import { CardSchema } from './content.config'; export type CardProps = z.infer<typeof CardSchema>; 
Enter fullscreen mode Exit fullscreen mode

... but still struggled to make it work.