본문으로 건너뛰기 Zod Complete Guide | TypeScript Schemas

Zod Complete Guide | TypeScript Schemas

Zod Complete Guide | TypeScript Schemas

이 글의 핵심

Zod bridges TypeScript types and runtime data. Define a schema once, infer static types with z.infer, and validate unknown JSON, env vars, and form input — without maintaining parallel validation logic.

What This Guide Covers

You will learn how to:

  • Model data with primitives, objects, unions, and enums
  • Add refinements and transforms (e.g., string → number)
  • Use safeParse vs parse at boundaries
  • Validate HTTP bodies and environment variables
  • Pair Zod with React Hook Form

1. Basics: parse vs safeParse

import { z } from 'zod';

const emailSchema = z.string().email();

// Throws ZodError on failure
emailSchema.parse('hi@example.com');

// Returns { success, data } | { success, error }
const result = emailSchema.safeParse('not-an-email');
if (!result.success) {
  console.error(result.error.flatten());
} else {
  console.log(result.data);
}

2. Objects & Type Inference

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  age: z.number().int().nonnegative().optional(),
  role: z.enum(['user', 'admin']),
});

type User = z.infer<typeof UserSchema>;

Nested objects and arrays compose naturally:

const AddressSchema = z.object({
  line1: z.string(),
  city: z.string(),
  zip: z.string().regex(/^\d{5}$/),
});

const OrderSchema = z.object({
  id: z.string(),
  items: z.array(z.object({ sku: z.string(), qty: z.number().int().positive() })).min(1),
  shipTo: AddressSchema,
});

3. Unions, Discriminated Unions, and Literals

const EventSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
  z.object({ type: z.literal('key'), key: z.string() }),
]);

4. Transforms & Coercion

const QuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  q: z.string().trim().optional(),
});

// "3" from query strings becomes number 3
QuerySchema.parse({ page: '3', q: '  hello  ' });

Use .transform for custom mapping:

const IsoDateSchema = z.string().transform((s) => new Date(s));

5. Refinements (Cross-Field Rules)

const PasswordSchema = z
  .object({
    password: z.string().min(8),
    confirm: z.string(),
  })
  .refine((data) => data.password === data.confirm, {
    message: 'Passwords must match',
    path: ['confirm'],
  });

6. API Request Validation (Express-Style)

import type { Request, Response } from 'express';

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2),
});

export async function createUser(req: Request, res: Response) {
  const parsed = CreateUserSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({
      error: 'Validation failed',
      issues: parsed.error.issues,
    });
  }
  const { email, name } = parsed.data;
  // ... persist
  return res.status(201).json({ ok: true });
}

7. Environment Variables

const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']),
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().default(3000),
});

export const env = EnvSchema.parse(process.env);

Fail fast at startup instead of deep in the request path.


8. React Hook Form

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

const FormSchema = z.object({
  title: z.string().min(3),
  accept: z.literal(true, {
    errorMap: () => ({ message: 'You must accept the terms' }),
  }),
});

type FormValues = z.infer<typeof FormSchema>;

export function PostForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: zodResolver(FormSchema),
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register('title')} />
      {errors.title && <p>{errors.title.message}</p>}
      <label>
        <input type="checkbox" {...register('accept')} /> I agree
      </label>
      {errors.accept && <p>{errors.accept.message}</p>}
      <button type="submit">Save</button>
    </form>
  );
}

9. Error Shapes for Clients

const result = UserSchema.safeParse(payload);
if (!result.success) {
  const flat = result.error.flatten();
  // flat.fieldErrors, flat.formErrors
}

For APIs, return error.issues (or a flattened map) so clients can show field-level messages.


10. Best Practices

Do

  • Validate at every trust boundary (HTTP, queue messages, CLI args).
  • Keep schemas next to the API contract or feature module.
  • Prefer safeParse at IO edges; reserve parse for tests and internal helpers.

Avoid

  • Reusing huge generic schemas for every endpoint — split by use case.
  • Validating giant blobs synchronously on every tick — batch or debounce when needed.

Summary & Checklist

  • Schema = single source of truth for runtime + types (z.infer).
  • safeParse for user-facing failures; parse when failure should be fatal.
  • Transforms and refinements encode business rules declaratively.
  • React Hook Form + zodResolver keeps UI errors aligned with server rules.

Checklist

  • Replace ad-hoc if checks with Zod on critical paths
  • Add EnvSchema.parse at startup
  • Return structured issues from APIs on 400
  • Share form schemas between client and server when possible (monorepo)

More career guides (Korean on pkglog.com)

Deep dives in Korean: tech interview prep, resume & interviews, practical job search.

Related posts:


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Master Zod for TypeScript: primitives, objects, refinements, transforms, safe parsing, React Hook Form integration, API … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Zod, TypeScript, Validation, Schema, React Hook Form, API 등으로 검색하시면 이 글이 도움이 됩니다.