Skip to content
intermediate Phase 10 · TypeScript Schema Validation

Zod Schema Validation

Define and validate runtime data with Zod schemas and type inference.

1h
0 problems
Topic Progress 0%

Schemas as the Single Source of Truth

Schemas as the Single Source of Truth

TypeScript types vanish at runtime; a Zod schema validates and derives the type:

import { z } from "zod";

const User = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().int().min(0).optional(),
  role: z.enum(["admin", "editor", "viewer"]),
});

type User = z.infer<typeof User>;   // compile-time type from the same schema

const r = User.safeParse(input);    // no throw: {success:true,data} | {success:false,error}
if (r.success) use(r.data);         // data: User
else report(r.error.issues);        // [{path, message, code}, ...]

parse throws a ZodError; safeParse returns a discriminated result — prefer it at boundaries where one bad payload must not crash the process. Because z.infer reads the schema, adding a field updates validation, the type, and every call site in one edit.

Composition: Unions, Refine, Transform

Composition: Unions, Refine, Transform

const Event = z.discriminatedUnion("kind", [
  z.object({ kind: z.literal("created"), at: z.string().datetime() }),
  z.object({ kind: z.literal("deleted"), reason: z.string().min(1) }),
]);   // narrowed by the literal "kind"

const Slug = z.string()
  .min(3).max(80)
  .regex(/^[a-z0-9-]+$/, "lowercase kebab only")
  .refine((s) => !s.endsWith("-"), { message: "no trailing dash" });

const Port = z.coerce.number().int().min(1).max(65535).default(3000);

const Trimmed = z.string().transform((s) => s.trim());

Pick z.union for ad-hoc alternatives (tries each) and z.discriminatedUnion when a shared literal tag exists — it fails fast on the tag instead of attempting every branch. transform maps after validation, refine/superRefine add cross-field rules that primitives cannot express.

Pitfalls and Interview Angles

Pitfalls and Interview Angles

  • Unknown keys: z.object strips unknown keys by default; strict rejects, loose passes through. Decide deliberately — dropping hides client bugs, accepting widens the surface.
  • Coercion boundaries: query strings and env vars arrive as strings — coerce at the boundary, never inside domain code where it masks earlier mistakes.
  • Error reading: use error.issues with path so the UI can highlight user.email.
  • Performance: parsing is linear in payload size — reuse schema constants and cap payload size.
  • Type drift: never hand-write interface User alongside the schema — one will rot. Define once, z.infer everywhere, and put the schema where the boundary is.

Interview angle: validate a request body, coerce the string port, narrow a discriminated event union, and explain stripping versus rejecting unknown keys.

Quiz

1. What does z.infer<typeof User> produce?

Question 1 options

2. Default behavior of z.object for keys present in input but absent from the schema?

Question 2 options

3. Why prefer z.discriminatedUnion over z.union here?

Question 3 options

Flashcards

Question

z.infer

Answer

Compile-time type extracted from a schema — runtime validation and static types from one definition.

Question

parse vs safeParse

Answer

parse throws ZodError; safeParse returns {success, data | error} — use at untrusted boundaries.

Question

z.coerce

Answer

Converts strings/numbers at the edge (env, query) before numeric validation rules run.

Question

Unknown-key modes

Answer

Default strips unknown keys; strict rejects; loose passes them through.

Revision Notes

Key Takeaways

  • 1. One schema drives runtime validation, inferred types, and error paths
  • 2. discriminatedUnion + literal tags give TS narrowing on untrusted payloads
  • 3. Decide strip vs strict vs loose for unknown keys, and coerce only at boundaries

Interview Tips

  • • Show safeParse handling and issue.path-mapped error messages as your default boundary code
  • • Explain why a hand-written interface beside a schema is a drift bug waiting to happen

Cheat Sheet

z.object().safeParse → {success,data|error}; type T = z.infer; discriminatedUnion("kind",[...]) + z.literal; coerce at edges; default strips unknown keys.

Continue Learning