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" }

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

  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).

Similar Posts