Home > Blog > tech

Zod คืออะไร? สอน Runtime Type Validation สำหรับ TypeScript Developer 2026

zod runtime type validation guide
Zod Runtime Type Validation Guide 2026
2026-04-11 | tech | 3500 words

ถ้าคุณเป็น TypeScript Developer คุณน่าจะเคยเจอปัญหานี้ — TypeScript ตรวจสอบ Type ได้เฉพาะตอน Compile เท่านั้น แต่เมื่อ Runtime จริงๆ ข้อมูลที่รับเข้ามาจาก API, form, environment variables หรือ database อาจไม่ตรงกับ Type ที่คุณกำหนดไว้เลย นั่นหมายความว่าแอปของคุณอาจ crash ได้ทุกเมื่อ แม้ TypeScript จะไม่แจ้ง Error ใดๆ

นี่คือเหตุผลที่ Zod ถูกสร้างขึ้นมา Zod เป็น TypeScript-first schema declaration and validation library ที่ช่วยให้คุณตรวจสอบข้อมูลได้ทั้งตอน Compile time และ Runtime ในคราวเดียว ปี 2026 Zod กลายเป็น standard ของ TypeScript ecosystem ไปแล้ว ไม่ว่าจะเป็น Next.js, tRPC, Hono, React Hook Form ล้วนรองรับ Zod อย่างเป็นทางการ

บทความนี้จะพาคุณเรียนรู้ Zod ตั้งแต่พื้นฐานจนถึงการใช้งานจริงในโปรเจกต์ Production ครอบคลุมทุก use case ที่สำคัญ พร้อมเปรียบเทียบกับ library อื่นๆ อย่างละเอียด

Zod คืออะไร?

Zod คือ TypeScript-first schema validation library ที่สร้างโดย Colin McDonnell ออกแบบมาเพื่อแก้ปัญหาช่องว่างระหว่าง TypeScript types (ที่หายไปตอน Runtime) กับ data validation ที่ต้องทำจริงใน production โดยมีคุณสมบัติหลักคือ:

// ติดตั้ง Zod
npm install zod

// Basic usage
import { z } from "zod";

// สร้าง schema
const UserSchema = z.object({
  name: z.string().min(1, "กรุณากรอกชื่อ"),
  email: z.string().email("อีเมลไม่ถูกต้อง"),
  age: z.number().int().min(18, "ต้องอายุ 18 ปีขึ้นไป"),
});

// Infer TypeScript type จาก schema
type User = z.infer<typeof UserSchema>;
// ได้ type: { name: string; email: string; age: number; }

// Validate ข้อมูล
const result = UserSchema.safeParse({
  name: "สมชาย",
  email: "somchai@example.com",
  age: 25,
});

if (result.success) {
  console.log(result.data); // type-safe User object
} else {
  console.log(result.error.issues); // array of validation errors
}
จุดเด่นสำคัญ: Zod ให้คุณเขียน schema ครั้งเดียว แล้วได้ทั้ง runtime validation และ TypeScript type โดยไม่ต้องเขียนซ้ำซ้อน นี่คือปัญหาที่ library อื่นๆ ไม่สามารถแก้ได้สมบูรณ์แบบเท่า Zod

Zod vs Joi vs Yup vs AJV — เปรียบเทียบ

ก่อนจะลงลึกในรายละเอียดของ Zod เรามาเปรียบเทียบกับ validation library ยอดนิยมตัวอื่นๆ กันก่อน เพื่อให้เข้าใจว่าทำไม Zod ถึงโดดเด่นในปี 2026

คุณสมบัติZodJoiYupAJV
TypeScript-firstใช่ไม่บางส่วนไม่
Type inferenceสมบูรณ์ไม่มีบางส่วนไม่มี
Bundle size~13KB~150KB~40KB~35KB
Dependencies0หลายตัวหลายตัวหลายตัว
Schema formatJS/TS chainJS chainJS chainJSON Schema
Error messagesดีมากดีดีปานกลาง
Transformsใช่ใช่ใช่จำกัด
Ecosystem 2026กว้างมากลดลงปานกลางเฉพาะกลุ่ม

Joi เคยเป็น king ของ Node.js validation แต่ถูกออกแบบมาก่อนยุค TypeScript จึงไม่มี type inference ที่ดี และ bundle size ใหญ่มากสำหรับ frontend Yup เคยเป็นตัวเลือกยอดนิยมสำหรับ React forms แต่ TypeScript support ไม่สมบูรณ์เท่า Zod ส่วน AJV ใช้ JSON Schema standard ซึ่งเหมาะกับ API documentation แต่ไม่เป็นมิตรกับ TypeScript developer ที่ต้องการ type safety

Zod Primitives — ชนิดข้อมูลพื้นฐาน

Zod รองรับ primitive types ทั้งหมดที่มีใน JavaScript/TypeScript โดยแต่ละตัวมี method สำหรับ validation เพิ่มเติมมากมาย

import { z } from "zod";

// String
const nameSchema = z.string()
  .min(2, "ชื่อต้องยาวอย่างน้อย 2 ตัวอักษร")
  .max(100, "ชื่อยาวเกินไป")
  .trim();

const emailSchema = z.string().email("อีเมลไม่ถูกต้อง");
const urlSchema = z.string().url("URL ไม่ถูกต้อง");
const uuidSchema = z.string().uuid();
const regexSchema = z.string().regex(/^[A-Z]{2}-\d{4}$/, "รหัสไม่ถูกรูปแบบ");

// Number
const ageSchema = z.number()
  .int("ต้องเป็นจำนวนเต็ม")
  .min(0, "อายุต้องไม่ติดลบ")
  .max(150, "อายุไม่สมเหตุสมผล");

const priceSchema = z.number().positive().finite();
const percentSchema = z.number().min(0).max(100);

// Boolean
const activeSchema = z.boolean();

// Date
const dateSchema = z.date()
  .min(new Date("2020-01-01"), "วันที่เก่าเกินไป")
  .max(new Date("2030-12-31"), "วันที่ไกลเกินไป");

// Enum
const roleSchema = z.enum(["admin", "editor", "viewer"]);
type Role = z.infer<typeof roleSchema>; // "admin" | "editor" | "viewer"

// Native Enum
enum Status { Active = "active", Inactive = "inactive" }
const statusSchema = z.nativeEnum(Status);

// Literal
const versionSchema = z.literal("v2");

// undefined, null, void, never, any, unknown
const nullableSchema = z.null();
const undefinedSchema = z.undefined();

Object Schemas — สร้าง Schema สำหรับ Object

Object schema เป็นหัวใจของ Zod เพราะข้อมูลส่วนใหญ่ในแอปจะอยู่ในรูป Object คุณสามารถกำหนดโครงสร้าง validate ข้อมูลซ้อนกันหลายชั้น และใช้ utility methods จัดการ schema ได้อย่างยืดหยุ่น

// Basic Object
const ProductSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  price: z.number().positive(),
  category: z.enum(["electronics", "clothing", "food"]),
  inStock: z.boolean().default(true),
  tags: z.array(z.string()).optional(),
});

type Product = z.infer<typeof ProductSchema>;

// Nested Object
const OrderSchema = z.object({
  orderId: z.string(),
  customer: z.object({
    name: z.string(),
    email: z.string().email(),
    address: z.object({
      street: z.string(),
      city: z.string(),
      zipCode: z.string().regex(/^\d{5}$/),
    }),
  }),
  items: z.array(ProductSchema),
  total: z.number().positive(),
  createdAt: z.date(),
});

// Utility Methods
const PartialProduct = ProductSchema.partial();       // ทุก field เป็น optional
const RequiredProduct = ProductSchema.required();     // ทุก field เป็น required
const ProductPreview = ProductSchema.pick({ name: true, price: true });
const ProductUpdate = ProductSchema.omit({ id: true, createdAt: true });

// Extend
const DetailedProduct = ProductSchema.extend({
  description: z.string(),
  images: z.array(z.string().url()),
});

// Merge two schemas
const AuditFields = z.object({
  createdBy: z.string(),
  updatedAt: z.date(),
});
const AuditedProduct = ProductSchema.merge(AuditFields);

// Strict mode — reject unknown keys
const StrictProduct = ProductSchema.strict();
// passthrough — keep unknown keys
const PassthroughProduct = ProductSchema.passthrough();
// strip — remove unknown keys (default)
const StrippedProduct = ProductSchema.strip();

Array Schemas — จัดการ Array

Zod มี method สำหรับ validate array ได้ละเอียดมาก ทั้งจำนวนสมาชิก ชนิดข้อมูลของสมาชิก และเงื่อนไขพิเศษต่างๆ

// Basic array
const tagsSchema = z.array(z.string());

// Array with constraints
const scoresSchema = z.array(z.number())
  .min(1, "ต้องมีคะแนนอย่างน้อย 1 รายการ")
  .max(10, "คะแนนได้สูงสุด 10 รายการ")
  .nonempty("ห้ามเป็น array ว่าง");

// Tuple — fixed-length array with specific types
const coordinateSchema = z.tuple([
  z.number(), // latitude
  z.number(), // longitude
]);

// Tuple with rest
const headerRowSchema = z.tuple([z.string(), z.string()]).rest(z.number());

// Set
const uniqueTagsSchema = z.set(z.string()).min(1).max(20);

// Map
const configMapSchema = z.map(z.string(), z.number());

// Record — dynamic keys
const scoresRecord = z.record(z.string(), z.number());
// validates: { "math": 95, "science": 88 }

Union และ Discriminated Union

Union type ช่วยให้คุณกำหนดว่าข้อมูลสามารถเป็นได้หลายรูปแบบ ซึ่งมีประโยชน์มากสำหรับ API response ที่มีหลายรูปแบบ หรือ form ที่เปลี่ยนฟิลด์ตามประเภทที่เลือก

// Basic Union
const stringOrNumber = z.union([z.string(), z.number()]);

// Discriminated Union — แนะนำสำหรับ object union เพราะ performance ดีกว่ามาก
const PaymentSchema = z.discriminatedUnion("method", [
  z.object({
    method: z.literal("credit_card"),
    cardNumber: z.string().regex(/^\d{16}$/),
    expiry: z.string(),
    cvv: z.string().length(3),
  }),
  z.object({
    method: z.literal("bank_transfer"),
    bankName: z.string(),
    accountNumber: z.string(),
  }),
  z.object({
    method: z.literal("promptpay"),
    phoneNumber: z.string().regex(/^0\d{9}$/),
  }),
]);

type Payment = z.infer<typeof PaymentSchema>;
// TypeScript จะ narrow type ตาม method field อัตโนมัติ

// Intersection — รวม schema (เหมือน TypeScript &)
const WithTimestamps = z.object({ createdAt: z.date(), updatedAt: z.date() });
const TimestampedUser = z.intersection(UserSchema, WithTimestamps);
ใช้ Discriminated Union แทน Union ธรรมดา: เมื่อ validate object ที่มี discriminator field (เช่น type, method, kind) ใช้ z.discriminatedUnion() เสมอ เพราะ Zod จะตรวจสอบ discriminator ก่อนแล้วเลือก schema ที่ถูกต้อง ทำให้ error message ชัดเจนกว่าและ performance ดีกว่ามาก

Optional, Nullable และ Default

การจัดการกับค่าที่อาจไม่มี หรืออาจเป็น null เป็นเรื่องที่ต้องเจอทุกวัน Zod จัดการเรื่องนี้ได้ง่ายและชัดเจน

// Optional — อาจไม่มี field นี้ (undefined)
const bioSchema = z.string().optional();
// type: string | undefined

// Nullable — อาจเป็น null
const middleNameSchema = z.string().nullable();
// type: string | null

// Nullish — อาจเป็น null หรือ undefined
const nicknameSchema = z.string().nullish();
// type: string | null | undefined

// Default — ใส่ค่า default ถ้าเป็น undefined
const roleSchema2 = z.string().default("viewer");
// input type: string | undefined
// output type: string

// Catch — ใส่ค่า fallback ถ้า validation ไม่ผ่าน
const safeAge = z.number().catch(0);
safeAge.parse("not a number"); // => 0

// Pipe + Coerce สำหรับแปลงค่า
const numericString = z.string().pipe(z.coerce.number());
numericString.parse("42"); // => 42

Transforms และ Refinements — แปลงและตรวจสอบขั้นสูง

นอกจาก validation ธรรมดาแล้ว Zod ยังสามารถ transform ข้อมูลระหว่าง validation ได้ด้วย ช่วยให้คุณ normalize ข้อมูลในขั้นตอนเดียวกัน

// Transform — แปลงค่า output
const lowercaseEmail = z.string().email().transform(val => val.toLowerCase());
// input: string, output: string (lowercase)

const csvToArray = z.string().transform(val => val.split(",").map(s => s.trim()));
// input: "a, b, c", output: ["a", "b", "c"]

const stringToDate = z.string().transform(val => new Date(val));
// input: "2026-04-11", output: Date object

// Refinement — custom validation
const passwordSchema = z.string()
  .min(8, "รหัสผ่านต้องยาวอย่างน้อย 8 ตัวอักษร")
  .refine(val => /[A-Z]/.test(val), "ต้องมีตัวพิมพ์ใหญ่อย่างน้อย 1 ตัว")
  .refine(val => /[0-9]/.test(val), "ต้องมีตัวเลขอย่างน้อย 1 ตัว")
  .refine(val => /[!@#$%^&*]/.test(val), "ต้องมีอักขระพิเศษอย่างน้อย 1 ตัว");

// Superrefine — เมื่อต้องการ control error เต็มที่
const registrationSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string(),
}).superRefine((data, ctx) => {
  if (data.password !== data.confirmPassword) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "รหัสผ่านไม่ตรงกัน",
      path: ["confirmPassword"],
    });
  }
});

// Preprocess — แปลงก่อน validate
const preprocessedNumber = z.preprocess(
  (val) => (typeof val === "string" ? parseInt(val, 10) : val),
  z.number().int().positive()
);
preprocessedNumber.parse("42"); // => 42

Error Handling — จัดการ Error อย่างมืออาชีพ

การจัดการ error ใน Zod ถูกออกแบบมาอย่างดี โดย ZodError ให้ข้อมูลละเอียดว่า validation ไม่ผ่านที่จุดไหน เพราะอะไร ช่วยให้คุณแสดง error message ให้ผู้ใช้ได้อย่างชัดเจน

// parse — throw ZodError ถ้าไม่ผ่าน
try {
  const user = UserSchema.parse(invalidData);
} catch (error) {
  if (error instanceof z.ZodError) {
    console.log(error.issues);
    // [{ code: "too_small", message: "...", path: ["name"], ... }]

    console.log(error.flatten());
    // { formErrors: [], fieldErrors: { name: ["..."], email: ["..."] } }

    console.log(error.format());
    // { name: { _errors: ["..."] }, email: { _errors: ["..."] } }
  }
}

// safeParse — ไม่ throw error คืน result object แทน (แนะนำ)
const result2 = UserSchema.safeParse(data);
if (!result2.success) {
  // result.error เป็น ZodError
  const fieldErrors = result2.error.flatten().fieldErrors;
  // { name: ["กรุณากรอกชื่อ"], email: ["อีเมลไม่ถูกต้อง"] }
}

// Custom error messages ภาษาไทย
const thaiUserSchema = z.object({
  name: z.string({ required_error: "กรุณากรอกชื่อ" }).min(1, "ชื่อห้ามว่าง"),
  email: z.string({ required_error: "กรุณากรอกอีเมล" }).email("รูปแบบอีเมลไม่ถูกต้อง"),
  phone: z.string().regex(/^0\d{9}$/, "หมายเลขโทรศัพท์ต้องเป็น 10 หลักเริ่มด้วย 0"),
});

// Custom error map — เปลี่ยน error message ทั้ง app
z.setErrorMap((issue, ctx) => {
  if (issue.code === z.ZodIssueCode.too_small) {
    return { message: `ต้องมีอย่างน้อย ${issue.minimum} ตัวอักษร` };
  }
  return { message: ctx.defaultError };
});

Zod กับ React Hook Form

การใช้ Zod กับ React Hook Form เป็นการผสมผสานที่ลงตัวที่สุดในปี 2026 สำหรับการทำ form validation ในแอป React โดยใช้ @hookform/resolvers/zod เป็นตัวเชื่อม

// ติดตั้ง
// npm install react-hook-form @hookform/resolvers zod

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

const ContactSchema = z.object({
  name: z.string().min(2, "ชื่อต้องยาวอย่างน้อย 2 ตัวอักษร"),
  email: z.string().email("อีเมลไม่ถูกต้อง"),
  message: z.string().min(10, "ข้อความต้องยาวอย่างน้อย 10 ตัวอักษร").max(500),
  topic: z.enum(["general", "support", "feedback"]),
});

type ContactForm = z.infer<typeof ContactSchema>;

function ContactPage() {
  const { register, handleSubmit, formState: { errors } } = useForm<ContactForm>({
    resolver: zodResolver(ContactSchema),
    defaultValues: { topic: "general" },
  });

  const onSubmit = (data: ContactForm) => {
    // data ถูก validate แล้ว — type-safe
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} />
      {errors.name && <span>{errors.name.message}</span>}

      <input {...register("email")} />
      {errors.email && <span>{errors.email.message}</span>}

      <textarea {...register("message")} />
      {errors.message && <span>{errors.message.message}</span>}

      <button type="submit">ส่ง</button>
    </form>
  );
}
ข้อดีของ Zod + React Hook Form: คุณเขียน schema ครั้งเดียว ได้ทั้ง form validation, TypeScript types, และ error messages โดยไม่ต้องเขียน validation logic ซ้ำซ้อน นอกจากนี้ยังแชร์ schema เดียวกันระหว่าง frontend กับ backend ได้อีกด้วย

Zod กับ tRPC

tRPC เป็น framework สำหรับสร้าง type-safe API ใน TypeScript โดยใช้ Zod เป็น default validation library สำหรับ input ของแต่ละ procedure ทำให้การทำงานกับ API เป็นแบบ end-to-end type safety ตั้งแต่ client ถึง server

// server/trpc.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";

const t = initTRPC.create();

const appRouter = t.router({
  // Query — ดึงข้อมูล
  getUser: t.procedure
    .input(z.object({ id: z.string().uuid() }))
    .query(async ({ input }) => {
      // input.id ถูก validate แล้ว — type: string (UUID)
      return await db.user.findUnique({ where: { id: input.id } });
    }),

  // Mutation — สร้าง/แก้ไขข้อมูล
  createPost: t.procedure
    .input(z.object({
      title: z.string().min(1).max(200),
      content: z.string().min(10),
      tags: z.array(z.string()).max(5).optional(),
      published: z.boolean().default(false),
    }))
    .mutation(async ({ input }) => {
      return await db.post.create({ data: input });
    }),

  // Pagination
  listPosts: t.procedure
    .input(z.object({
      page: z.number().int().positive().default(1),
      limit: z.number().int().min(1).max(100).default(20),
      search: z.string().optional(),
      category: z.enum(["tech", "life", "news"]).optional(),
    }))
    .query(async ({ input }) => {
      const skip = (input.page - 1) * input.limit;
      return await db.post.findMany({
        skip,
        take: input.limit,
        where: { title: { contains: input.search } },
      });
    }),
});

// client — ใช้งานฝั่ง client ได้แบบ type-safe
// const user = await trpc.getUser.query({ id: "..." });
// TypeScript รู้ว่า user มี type อะไร

Zod กับ Hono/Express Middleware

Zod สามารถใช้เป็น middleware สำหรับ validate request body, query params, headers ใน web framework ต่างๆ ได้ ช่วยให้ API ปลอดภัยจากข้อมูลผิดรูปแบบ

// ========== Hono (แนะนำสำหรับ 2026) ==========
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const app = new Hono();

const CreateUserBody = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(["admin", "user"]).default("user"),
});

const PaginationQuery = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
});

app.post("/api/users",
  zValidator("json", CreateUserBody),
  async (c) => {
    const body = c.req.valid("json"); // type-safe
    // สร้าง user...
    return c.json({ success: true, user: body });
  }
);

app.get("/api/users",
  zValidator("query", PaginationQuery),
  async (c) => {
    const { page, limit } = c.req.valid("query");
    // ดึงข้อมูล...
    return c.json({ users: [], page, limit });
  }
);

// ========== Express ==========
import express from "express";

function validateBody(schema: z.ZodSchema) {
  return (req: express.Request, res: express.Response, next: express.NextFunction) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        error: "Validation failed",
        details: result.error.flatten().fieldErrors,
      });
    }
    req.body = result.data; // ใช้ parsed + transformed data
    next();
  };
}

app.post("/api/users", validateBody(CreateUserBody), (req, res) => {
  // req.body ถูก validate แล้ว
  res.json({ success: true });
});

Zod กับ Prisma (zod-prisma-types)

เมื่อใช้ Prisma ORM คุณสามารถ generate Zod schemas จาก Prisma schema ได้อัตโนมัติ ช่วยให้ validation schema ตรงกับ database schema เสมอ ไม่ต้องเขียนซ้ำสอง

// ติดตั้ง
// npm install zod-prisma-types

// prisma/schema.prisma
// generator zod {
//   provider = "zod-prisma-types"
//   output   = "../src/zod"
// }
//
// model User {
//   id    String @id @default(uuid())
//   name  String
//   email String @unique
//   age   Int?
//   posts Post[]
// }

// สั่ง generate
// npx prisma generate

// ใช้งาน — schema ถูก generate มาให้แล้ว
import { UserSchema, UserCreateInputSchema } from "../src/zod";

// validate ก่อน insert
const validatedData = UserCreateInputSchema.parse({
  name: "สมหญิง",
  email: "somying@example.com",
  age: 28,
});

await prisma.user.create({ data: validatedData });

การใช้ zod-prisma-types ช่วยลดปัญหา schema drift ซึ่งเป็นปัญหาที่ validation schema ไม่ตรงกับ database schema เพราะลืมอัปเดตฝั่งใดฝั่งหนึ่ง เมื่อคุณเปลี่ยน Prisma schema แล้ว generate ใหม่ Zod schemas จะอัปเดตตาม

Zod กับ Environment Variables (t3-env)

หนึ่งในการใช้งาน Zod ที่มีประโยชน์มากที่สุดคือการ validate environment variables ด้วย t3-env ซึ่งช่วยให้แอปของคุณ fail fast เมื่อ env vars ไม่ครบหรือไม่ถูกต้อง แทนที่จะ crash ตอน runtime ทีหลัง

// ติดตั้ง
// npm install @t3-oss/env-nextjs zod

// src/env.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    JWT_SECRET: z.string().min(32),
    REDIS_URL: z.string().url().optional(),
    NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
    PORT: z.coerce.number().int().default(3000),
    SMTP_HOST: z.string().min(1),
    SMTP_PORT: z.coerce.number().int(),
    SMTP_USER: z.string().email(),
    SMTP_PASS: z.string().min(1),
  },
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
    NEXT_PUBLIC_APP_NAME: z.string().default("MyApp"),
    NEXT_PUBLIC_GA_ID: z.string().regex(/^G-/).optional(),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    JWT_SECRET: process.env.JWT_SECRET,
    REDIS_URL: process.env.REDIS_URL,
    NODE_ENV: process.env.NODE_ENV,
    PORT: process.env.PORT,
    SMTP_HOST: process.env.SMTP_HOST,
    SMTP_PORT: process.env.SMTP_PORT,
    SMTP_USER: process.env.SMTP_USER,
    SMTP_PASS: process.env.SMTP_PASS,
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
    NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
    NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
  },
});

// ใช้งาน — type-safe และ validated
// import { env } from "./env";
// const dbUrl = env.DATABASE_URL; // string — guaranteed to be valid URL
Fail Fast: ถ้า env var ใดไม่ผ่าน validation แอปจะไม่ start เลย พร้อมแสดง error message ชัดเจนว่าตัวแปรไหนมีปัญหา ดีกว่าปล่อยให้ crash กลางทาง runtime ตอนที่ user กำลังใช้งานอยู่

z.infer — สร้าง TypeScript Types จาก Schema

z.infer เป็นฟีเจอร์สำคัญที่สุดของ Zod ช่วยให้คุณไม่ต้องเขียน TypeScript type แยกจาก validation schema ลดการเขียนซ้ำซ้อนและป้องกัน type กับ validation ไม่ตรงกัน

// แทนที่จะเขียนแบบนี้ (ซ้ำซ้อน!):
interface User {
  name: string;
  email: string;
  age: number;
}
// + validation schema แยก...

// ใช้ Zod เขียนครั้งเดียว:
const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().int().positive(),
});

// สร้าง type อัตโนมัติ
type User = z.infer<typeof UserSchema>;
// ได้เหมือนกับ interface ด้านบนเป๊ะ

// Input vs Output types (สำหรับ schema ที่มี transform)
const ProcessedUser = z.object({
  name: z.string().transform(s => s.trim().toLowerCase()),
  joinDate: z.string().transform(s => new Date(s)),
});

type ProcessedUserInput = z.input<typeof ProcessedUser>;
// { name: string; joinDate: string; }

type ProcessedUserOutput = z.output<typeof ProcessedUser>;
// { name: string; joinDate: Date; }

// z.infer = z.output (ใช้ output type เป็น default)

Coercion — แปลงค่าอัตโนมัติ

เมื่อรับข้อมูลจาก query string, form data หรือ environment variables ข้อมูลจะเป็น string เสมอ Zod coercion ช่วยแปลงค่าให้เป็นชนิดที่ต้องการโดยอัตโนมัติ

// Coercion schemas
const coercedNumber = z.coerce.number();
coercedNumber.parse("42");      // => 42
coercedNumber.parse("3.14");    // => 3.14

const coercedBoolean = z.coerce.boolean();
coercedBoolean.parse("true");   // => true
coercedBoolean.parse(1);        // => true
coercedBoolean.parse("false");  // => true (เพราะ Boolean("false") = true!)

const coercedDate = z.coerce.date();
coercedDate.parse("2026-04-11");   // => Date object
coercedDate.parse(1713000000000);  // => Date object

const coercedBigint = z.coerce.bigint();
coercedBigint.parse("12345678901234567890"); // => BigInt

// ใช้กับ query params
const SearchParams = z.object({
  q: z.string().optional(),
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  active: z.string().transform(v => v === "true").default("true"),
});

Custom Validators และ Lazy Schemas

สำหรับ use case ที่ซับซ้อนกว่าปกติ Zod มี custom validators และ lazy schemas สำหรับ recursive data structures

// Custom Validator — สร้าง reusable schema
const thaiPhoneNumber = z.string()
  .regex(/^0[689]\d{8}$/, "เบอร์โทรไม่ถูกต้อง (ต้องเริ่มด้วย 06, 08, 09)")
  .transform(val => val.replace(/^0/, "+66"));

const thaiIdCard = z.string()
  .length(13, "เลขบัตรประชาชนต้องมี 13 หลัก")
  .regex(/^\d{13}$/, "ต้องเป็นตัวเลขเท่านั้น")
  .refine((val) => {
    // Validate Thai ID checksum
    let sum = 0;
    for (let i = 0; i < 12; i++) {
      sum += parseInt(val[i]) * (13 - i);
    }
    const check = (11 - (sum % 11)) % 10;
    return check === parseInt(val[12]);
  }, "เลขบัตรประชาชนไม่ถูกต้อง");

// Lazy Schema — สำหรับ recursive structures
interface Category {
  name: string;
  children: Category[];
}

const CategorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    children: z.array(CategorySchema),
  })
);

// ใช้กับ tree structure เช่น menu, comments, file system
const menuData = CategorySchema.parse({
  name: "root",
  children: [
    { name: "เทคโนโลยี", children: [
      { name: "JavaScript", children: [] },
      { name: "Python", children: [] },
    ] },
    { name: "วิทยาศาสตร์", children: [] },
  ],
});

// Brand types — สร้าง nominal type
const UserId = z.string().uuid().brand<"UserId">();
type UserId = z.infer<typeof UserId>;
// ไม่สามารถใช้ string ธรรมดาแทน UserId ได้

Performance Considerations

แม้ Zod จะเป็น library ที่ยอดเยี่ยม แต่ก็ต้องพิจารณาเรื่อง performance ด้วย โดยเฉพาะเมื่อใช้กับข้อมูลจำนวนมากหรือ hot paths ที่ต้องทำงานเร็วมาก

// 1. Parse once, use many — อย่า parse ซ้ำ
const schema = z.object({ name: z.string(), age: z.number() });
const validated = schema.parse(data); // parse ครั้งเดียว
// ใช้ validated ต่อไป อย่า parse อีก

// 2. ใช้ safeParse แทน try-catch
// safeParse เร็วกว่า parse + try-catch เมื่อ data ไม่ผ่าน
const result3 = schema.safeParse(data);

// 3. ใช้ discriminatedUnion แทน union สำหรับ objects
// discriminatedUnion ตรวจ discriminator field ก่อน ไม่ต้อง try ทุก variant

// 4. Lazy evaluation — schema ถูกสร้างตอน import
// ถ้ามี schema เยอะมาก พิจารณา lazy loading

// 5. สำหรับ data จำนวนมาก (เช่น 10k+ rows)
// พิจารณาใช้ AJV (JSON Schema) หรือ TypeBox ที่เร็วกว่า
// Zod เหมาะกับ API input validation (ข้อมูลไม่เยอะ)
// ไม่เหมาะกับ batch processing ข้อมูลจำนวนมหาศาล

Zod vs TypeBox vs Valibot

ในปี 2026 มี TypeScript validation library ใหม่ๆ เกิดขึ้นมาท้าชิงตำแหน่งของ Zod โดยเฉพาะ TypeBox และ Valibot ที่เน้นเรื่อง performance และ bundle size

คุณสมบัติZodTypeBoxValibot
Bundle size (min)~13KB~8KB~1-5KB*
Performanceดีเร็วมากดี
Type inferenceสมบูรณ์สมบูรณ์สมบูรณ์
API styleMethod chainFunction callFunction pipe
JSON Schemaต้องใช้ pluginNativeต้องใช้ plugin
Ecosystemใหญ่ที่สุดปานกลางเติบโต
Tree-shakableไม่บางส่วนใช่
Learning curveง่ายปานกลางง่าย

* Valibot ใช้ tree-shaking ดังนั้น bundle size ขึ้นอยู่กับจำนวน feature ที่ใช้จริง

TypeBox เหมาะสำหรับโปรเจกต์ที่ต้องการ performance สูงสุดและใช้ JSON Schema standard เช่น Fastify ซึ่งมี TypeBox built-in มี compile step ที่ทำให้ validation เร็วกว่า Zod หลายเท่า แต่ API style อาจไม่คุ้นเคยสำหรับคนที่ชินกับ method chaining

Valibot เป็นทางเลือกที่เน้น bundle size เล็กมากผ่าน tree-shaking ทำให้จ่ายเฉพาะ feature ที่ใช้จริง เหมาะสำหรับ frontend ที่ต้องการ bundle เล็กที่สุด API คล้าย Zod แต่ใช้ function composition แทน method chaining

สำหรับโปรเจกต์ส่วนใหญ่ในปี 2026 Zod ยังคงเป็นตัวเลือกที่ดีที่สุดเพราะ ecosystem กว้างที่สุด มี integration กับทุก framework ที่นิยม และ community ใหญ่ที่สุด แต่ถ้าคุณต้องการ performance สูงสุดหรือ bundle size เล็กที่สุด ก็พิจารณา TypeBox หรือ Valibot ตามลำดับ

Best Practices สำหรับ Zod ในโปรเจกต์จริง

หลังจากเรียนรู้ API ทั้งหมดแล้ว มาดู best practices สำหรับการใช้ Zod ในโปรเจกต์จริงกัน

// 1. จัดโครงสร้าง schema files
// src/schemas/user.schema.ts
// src/schemas/product.schema.ts
// src/schemas/order.schema.ts
// src/schemas/common.schema.ts (shared schemas)

// 2. สร้าง reusable base schemas
const IdSchema = z.string().uuid();
const EmailSchema = z.string().email().toLowerCase();
const PaginationSchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  sortBy: z.string().optional(),
  sortOrder: z.enum(["asc", "desc"]).default("desc"),
});

// 3. แยก Create / Update schemas
const UserCreateSchema = z.object({
  name: z.string().min(1),
  email: EmailSchema,
  password: z.string().min(8),
});

const UserUpdateSchema = UserCreateSchema.partial().omit({ password: true });

// 4. ใช้ brand types สำหรับ ID ต่างๆ
const UserId = z.string().uuid().brand<"UserId">();
const PostId = z.string().uuid().brand<"PostId">();
// ป้องกันส่ง PostId ไปแทน UserId

// 5. Export ทั้ง schema และ type
export const UserSchema = z.object({ /* ... */ });
export type User = z.infer<typeof UserSchema>;

// 6. Version API schemas
export const UserSchemaV1 = z.object({ /* v1 fields */ });
export const UserSchemaV2 = UserSchemaV1.extend({ /* v2 additions */ });
Single Source of Truth: ให้ Zod schema เป็นแหล่งข้อมูลหลักสำหรับทั้ง validation rules และ TypeScript types อย่าเขียน interface/type แยกจาก schema เพราะจะทำให้เกิดปัญหา out-of-sync ในอนาคต

สรุป

Zod เป็น library ที่เปลี่ยนวิธีคิดเรื่อง data validation ใน TypeScript ไปอย่างสิ้นเชิง จากเดิมที่ต้องเขียน TypeScript types กับ validation logic แยกกัน ตอนนี้คุณเขียนครั้งเดียวด้วย Zod แล้วได้ทั้งสองอย่าง ทำให้โค้ดสั้นลง ผิดพลาดน้อยลง และ maintain ง่ายขึ้นมาก

ในปี 2026 Zod ได้กลายเป็นส่วนสำคัญของ TypeScript ecosystem ไม่ว่าจะใช้กับ React Hook Form สำหรับ form validation ใช้กับ tRPC สำหรับ type-safe API ใช้กับ Hono หรือ Express สำหรับ middleware validation ใช้กับ Prisma สำหรับ database schema sync หรือใช้กับ t3-env สำหรับ environment variables ทุกอย่างทำงานร่วมกันได้อย่างลงตัว

ถ้าคุณยังไม่ได้ใช้ Zod วันนี้เป็นวันที่ดีที่สุดในการเริ่มต้น ลองติดตั้ง Zod ในโปรเจกต์ของคุณ เริ่มจาก validate API input หรือ form data แล้วคุณจะเห็นว่า runtime type safety ช่วยให้แอปของคุณมีความเสถียรและ debug ง่ายขึ้นมากเพียงใด


Back to Blog | iCafe Forex | SiamLanCard | Siam2R