Runtime Types with Zod

One Schema → Validation AND Types

Runtime Types with Zod

TS types are erased at runtime — useless for validating untrusted input. Zod gives you a schema that both validates AND infers the TS type.

4 min read Level 2/5 #typescript#zod#validation
What you'll learn
  • Define a Zod schema
  • Validate at the system boundary
  • Use `z.infer` to derive the TS type

TS types vanish at compile time. They can’t validate user input, JSON over the wire, or env vars. For that you need runtime validation. Zod is the standard tool — one schema gives you both the runtime check AND the inferred TS type.

Install

npm install zod

A Schema

import { z } from "zod";

const UserSchema = z.object({
  id:    z.string().uuid(),
  email: z.string().email(),
  age:   z.number().int().min(0),
});

type User = z.infer<typeof UserSchema>;
// { id: string; email: string; age: number }

z.infer<typeof Schema> gives you the TS type — no duplication.

Validating Input

const raw = JSON.parse(rawJson);   // unknown shape

const result = UserSchema.safeParse(raw);
if (!result.success) {
  console.error(result.error.format());
  return;
}

const user: User = result.data;   // typed AND validated

safeParse returns a discriminated union — { success: true, data: User } or { success: false, error }. No throws.

Validation Where It Matters

The right place for validation is the system boundary — where data first enters your trust zone:

  • HTTP request bodies
  • Form input
  • localStorage / sessionStorage values you read back
  • Parsed JSON from a file or API
  • Env vars

Inside your code — past that boundary — trust the types.

Composition

Schemas compose like types:

const PostSchema = z.object({
  id: z.string().uuid(),
  title: z.string().min(1),
  author: UserSchema,             // nested
  tags: z.array(z.string()),
});

const PostListSchema = z.array(PostSchema);

Refinements

const PasswordSchema = z
  .string()
  .min(8)
  .refine(p => /[A-Z]/.test(p), "must contain an uppercase letter")
  .refine(p => /[0-9]/.test(p), "must contain a digit");

Custom checks that go beyond shape.

Why Not Just TS?

Because TS types don’t exist at runtime. A JSON.parse of an attacker-controlled payload that’s typed as User is a security bug. Zod fixes that gap.

Up Next

Migrating a JS codebase to TS — the gradual path.

Migrating From JS →