Zod: ตัวช่วยสำคัญที่ทำให้ TypeScript ปลอดภัยขึ้นตอนรันไทม์
แม้ TypeScript จะช่วยตรวจสอบชนิดข้อมูลได้ดีในขั้นตอนคอมไพล์ แต่ข้อมูลจริงจากฟอร์ม, API หรือ query string ยังอาจผิดรูปแบบได้เสมอ Zod จึงเข้ามาเติมช่องว่างด้วยการตรวจสอบข้อมูลตอนรันไทม์ พร้อมอนุมาน type ให้ใช้งานร่วมกับ Ty

Zod: ตัวช่วยสำคัญที่ทำให้ TypeScript ปลอดภัยขึ้นตอนรันไทม์
หลายทีมพัฒนาเลือกใช้ TypeScript เพื่อเพิ่มความมั่นใจเรื่องชนิดข้อมูลระหว่างเขียนโค้ด แต่ในโลกความจริง ปัญหามักไม่ได้เกิดจาก type definition ในไฟล์ของเราเท่านั้น หากเกิดจาก “ข้อมูลที่วิ่งเข้าระบบ” ซึ่งอาจมาจากฟอร์ม, query string, API, webhook หรือ localStorage และข้อมูลเหล่านี้มักไม่ได้อยู่ในรูปแบบที่เราคาดหวังเสมอไป
นี่คือจุดที่ Zod กลายเป็นเครื่องมือที่น่าจับตา เพราะมันช่วยให้เราสร้างสคีมาสำหรับตรวจสอบข้อมูลตอนรันไทม์ และยังอนุมาน type ให้ TypeScript ได้ในเวลาเดียวกัน
ทำไม TypeScript อย่างเดียวจึงยังไม่พอ
TypeScript เด่นมากในเรื่องการตรวจสอบข้อผิดพลาดตอนคอมไพล์ ช่วยให้เราเห็นปัญหาก่อนรันโปรแกรมจริง แต่เมื่อแอปพลิเคชันเริ่มรับข้อมูลจากภายนอก สถานการณ์จะเปลี่ยนทันที
ตัวอย่างแหล่งข้อมูลที่เสี่ยงมีได้ เช่น
- ข้อมูลจากฟอร์มผู้ใช้
- query parameters จาก URL
- response จาก API ภายนอก
- webhook จากระบบอื่น
- ข้อมูลใน localStorage
แม้เราจะเขียน type ไว้อย่างดี แต่ข้อมูลเหล่านี้ยังอาจเป็น unknown หรือ any ได้เสมอ หากไม่มีการตรวจสอบก่อนใช้งาน ก็อาจนำไปสู่บั๊กที่หาได้ยาก
Zod ช่วยแก้ปัญหาอย่างไร
แนวคิดหลักของ Zod คือการสร้าง schema เพื่อกำหนดรูปแบบข้อมูลที่ถูกต้อง จากนั้นใช้ schema เดียวกันเพื่อตรวจสอบข้อมูลจริงในตอนรันไทม์ และสร้าง type สำหรับ TypeScript โดยอัตโนมัติ
สิ่งที่ทำให้หลายคนชอบ Zod มีดังนี้
- เขียน schema ครั้งเดียว แล้วได้ type กลับมาอัตโนมัติ
- ข้อความ error อ่านง่าย และระบุ field ที่ผิดได้ชัดเจน
- ใช้ schema เดียวกันได้ทั้งกับฟอร์มและ API ลดโค้ดซ้ำ
- รองรับ
default,transform,refineและการปรับแต่งข้อมูลในจุดเดียว
ตัวอย่าง schema สำหรับฟอร์มสมัครสมาชิก
ตัวอย่างต่อไปนี้เป็น schema ง่าย ๆ สำหรับงานสมัครสมาชิก
import { z } from "zod"
const SignUpSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
age: z.coerce.number().int().min(18).optional(),
marketing: z.boolean().default(false),
})
type SignUp = z.infer<typeof SignUpSchema>
จากโค้ดนี้ เราจะได้ประโยชน์หลายอย่างพร้อมกัน
emailต้องเป็นสตริงที่อยู่ในรูปแบบอีเมลpasswordต้องมีความยาวอย่างน้อย 8 ตัวอักษรageสามารถรับค่าจาก input ที่เป็นสตริง เช่น"18"แล้วแปลงเป็นตัวเลขก่อนตรวจสอบmarketingมีค่าเริ่มต้นเป็นfalseSignUpถูกอนุมาน type จาก schema โดยตรง ทำให้ไม่ต้องประกาศ type ซ้ำ
จุดที่มือใหม่มักไม่รู้: coerce ช่วยได้มาก
หนึ่งในฟีเจอร์ที่มีประโยชน์มากคือ z.coerce.number() เพราะข้อมูลจากฟอร์มหรือ query params มักมาเป็นสตริง แม้ค่าจริงจะเป็นตัวเลขก็ตาม
ตัวอย่างเช่น
age: z.coerce.number().int().min(18).optional()
หากผู้ใช้กรอก "18" เข้ามา Zod จะพยายามแปลงให้เป็น 18 ก่อนตรวจสอบเงื่อนไขต่อ ทำให้ลดงานแปลงค่าด้วยตัวเอง และลดโอกาสเกิดบั๊กจากการ parse แบบ manual
วิธีรับข้อมูลจากภายนอกอย่างปลอดภัย
เมื่อรับข้อมูลจาก request หรือแหล่งภายนอก ควรให้ schema เป็นด่านแรกเสมอ
ใช้ parse เมื่ออยากให้ throw error
try {
const data = SignUpSchema.parse(req.body)
// data มี type SignUp และผ่านการ validate แล้ว
} catch (e) {
// e เป็น ZodError: เอาไปทำ response 400 ได้
}
แนวทางนี้เหมาะกับกรณีที่ต้องการหยุดการทำงานทันทีเมื่อข้อมูลไม่ถูกต้อง และจัดการ error ในจุดเดียว
ใช้ safeParse เมื่อไม่อยาก throw
const result = SignUpSchema.safeParse(req.body)
if (!result.success) {
// result.error.flatten() เอาไปทำ error รายช่องได้
} else {
// result.data ใช้งานต่อได้เลย
}
วิธีนี้เหมาะกับงานฟอร์มและ API ที่ต้องการจัดการผลลัพธ์แบบชัดเจน โดยเฉพาะเมื่ออยากส่งข้อความผิดพลาดกลับไปยังแต่ละช่องของฟอร์ม
ทริคที่ช่วยลดบั๊กใน API อย่างเห็นผล
การใช้ Zod ให้ได้ประโยชน์สูงสุด ไม่ได้มีแค่เขียน schema พื้นฐานเท่านั้น แต่ควรออกแบบให้ตรงกับขอบเขตของข้อมูลในระบบด้วย
1) สร้าง schema ที่ขอบเขตของ endpoint
อย่าปล่อยให้ controller รับค่าแบบ any แล้วค่อยตรวจสอบทีหลัง ควร validate ตั้งแต่จุดที่ข้อมูลเข้าสู่ระบบ เพื่อให้ข้อมูลที่หลุดเข้าชั้น business logic เป็นข้อมูลที่เชื่อถือได้แล้ว
2) แยก schema ระหว่าง request และ response
ฝั่ง request มักเหมาะกับการใช้ coerce เพราะข้อมูลจากภายนอกอาจยังไม่อยู่ในชนิดที่ต้องการ ส่วนฝั่ง response ควรเข้มงวดมากขึ้น เช่นใช้ strict() หรือ transform() เพื่อให้รูปแบบข้อมูลตรงตามสเปกที่ API สัญญาไว้
3) ใช้ strict() กัน field แปลกปลอม
const Payload = z.object({
id: z.string().uuid(),
role: z.enum(["user", "admin"]),
}).strict()
การใช้ strict() ช่วยป้องกันกรณีมี field ที่ไม่ได้คาดไว้หลุดเข้ามาใน payload ซึ่งเป็นเรื่องสำคัญสำหรับระบบที่ต้องการความปลอดภัยและความชัดเจนของสัญญาข้อมูล
4) ใช้ branded type แยก string ที่ความหมายต่างกัน
แม้ userId และ orderId จะเป็น string เหมือนกัน แต่ในเชิงธุรกิจเราไม่ควรสลับกันได้ง่าย ๆ
const UserId = z.string().uuid().brand("UserId")
type UserId = z.infer<typeof UserId>
แนวทางนี้ช่วยเพิ่มความหมายให้ type และลดความผิดพลาดจากการใช้ค่าคนละบริบทปะปนกัน
5) ใช้ preprocess หรือ transform ลดงาน parsing ด้วยมือ
Zod สามารถช่วยจัดการข้อมูลก่อนหรือหลัง validation ได้ เช่น
- ตัดช่องว่างหน้า-หลังด้วย
trim() - แปลงอีเมลเป็นตัวพิมพ์เล็กด้วย
toLowerCase() - แปลงข้อความเป็นวันที่หรือรูปแบบอื่นที่ต้องการ
ตัวอย่าง
const Email = z.string().trim().toLowerCase().email()
วิธีนี้ช่วยให้กติกาเรื่องข้อมูลถูกรวมไว้ใน schema แทนที่จะกระจายอยู่หลายจุดในโค้ด
จุดเด่นที่ช่วยทีมทำงานเร็วขึ้นจริง
ข้อดีที่เห็นผลมากในงานจริงคือ Zod error บอกได้ชัดว่า field ไหนผิดและผิดเพราะอะไร ทำให้ทั้งทีมพัฒนาและ QA ไล่ปัญหาได้เร็วขึ้น
ผลลัพธ์ที่ตามมาคือ
- ลดเวลาหาสาเหตุของบั๊ก
- ลดการแก้โค้ดแบบเดาสุ่ม
- ลดโอกาสสร้างบั๊กใหม่ระหว่างแก้ของเดิม
- ทำให้รูปแบบการจัดการ error สม่ำเสมอทั้งระบบ
Flow ที่แนะนำสำหรับโปรเจกต์ใหม่
หากเริ่มโปรเจกต์ใหม่และอยากใช้ Zod อย่างคุ้มค่า สามารถเริ่มจากแนวทางนี้ได้
- เริ่มเขียน schema ให้กับ request ของ endpoint สำคัญก่อน
- นำ schema เดียวกันไปใช้กับฟอร์มฝั่งหน้าเว็บ หรือแชร์ผ่าน shared package
- กำหนดรูปแบบ response error ให้เป็นมาตรฐานเดียวกัน
- เขียน test เน้นเคสของ schema แล้วปล่อยให้ Zod เป็นด่านแรกในการกันข้อมูลผิดรูปแบบ
แนวคิดนี้ช่วยให้ schema กลายเป็นศูนย์กลางของกติกาข้อมูลทั้งระบบ และลดความซ้ำซ้อนระหว่าง frontend กับ backend
เหมาะกับ stack แบบไหนบ้าง
Zod ใช้งานร่วมกับเครื่องมือยอดนิยมได้ดี ไม่ว่าจะเป็น
- Next.js
- Express
- Fastify
- tRPC
สำหรับทีมที่ใช้ TypeScript อยู่แล้ว Zod ถือเป็นตัวเสริมที่ทำให้ type safety ไม่ได้หยุดอยู่แค่ตอนเขียนโค้ด แต่ต่อเนื่องไปจนถึงตอนข้อมูลวิ่งเข้าระบบจริง
สรุป
TypeScript ช่วยป้องกันข้อผิดพลาดได้ดีในขั้นตอนพัฒนา แต่เมื่อข้อมูลจากโลกภายนอกไหลเข้าสู่ระบบ เรายังต้องมีเครื่องมือสำหรับตรวจสอบในตอนรันไทม์ และนั่นคือบทบาทสำคัญของ Zod
เมื่อใช้ TypeScript ควบคู่กับ Zod เราจะได้ทั้งความมั่นใจระหว่างเขียนโค้ด และความปลอดภัยตอนรับข้อมูลจริง ช่วยลดบั๊กจากข้อมูลผิดรูปแบบได้อย่างชัดเจน โดยเฉพาะในงานฟอร์มและ API ที่ต้องพึ่งพาข้อมูลจากผู้ใช้หรือระบบภายนอกอยู่ตลอดเวลา