1 คะแนน โดย GN⁺ 2025-05-20 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Zod 4 เปิดตัวเป็นเวอร์ชันเสถียรหลังพัฒนามา 1 ปี แก้ข้อจำกัดด้านการออกแบบของ Zod 3 และปรับปรุง ประสิทธิภาพ ขนาดบันเดิล และประสิทธิภาพการคอมไพล์ TypeScript อย่างมาก
  • Zod 3 ซึ่งออกเมื่อเดือนพฤษภาคม 2021 เติบโตจาก GitHub star 2,700 รายการและยอดดาวน์โหลดรายสัปดาห์ 600,000 ครั้ง มาเป็น 37.8k stars และ ยอดดาวน์โหลดรายสัปดาห์ 31 ล้านครั้ง ในปัจจุบัน และหลังผ่าน minor version 24 รุ่น การปรับปรุงสำคัญจำเป็นต้องมี breaking changes
  • ใน benchmark การ parse สตริงเร็วขึ้น 14 เท่า, การ parse array เร็วขึ้น 7 เท่า, การ parse object เร็วขึ้น 6.5 เท่า และจำนวน type instantiation ของ tsc ในตัวอย่างง่าย ๆ ลดจากมากกว่า 25,000 ครั้งเหลือประมาณ 175 ครั้ง
  • ขนาดบันเดิลหลักแบบ gzip ลดจาก Zod 3 12.47kb เหลือ Zod 4 เวอร์ชันปกติ 5.36kb และ Zod Mini ซึ่งเป็น functional API ที่ tree-shaking ได้ ลดลงเหลือ 1.88kb
  • ฟีเจอร์ใหม่รวมถึง การแปลงเป็น JSON Schema, metadata registry, การอนุมาน recursive object type, internationalization, error pretty-printing, template literal type, z.stringbool(), พารามิเตอร์ error แบบรวมศูนย์ และ z.discriminatedUnion() ที่ปรับปรุงแล้ว

เหตุผลที่ Zod 4 เป็น major version ใหม่

  • Zod 4 กลายเป็นเวอร์ชันเสถียรหลังการพัฒนาอย่างต่อเนื่อง 1 ปี และนำฟีเจอร์ที่มีผู้ร้องขอมานานมาทำบนฐานที่เร็วกว่า เล็กกว่า และมีประสิทธิภาพกับ tsc มากกว่า
  • Zod v3.0 เปิดตัวในเดือนพฤษภาคม 2021 ตอนนั้นมี GitHub star 2,700 รายการ และยอดดาวน์โหลดรายสัปดาห์ 600,000 ครั้ง
  • ปัจจุบัน Zod มี GitHub star 37.8k และยอดดาวน์โหลดรายสัปดาห์ 31 ล้านครั้ง
    • เมื่อ 6 สัปดาห์ก่อนที่เบต้าออก ยอดดาวน์โหลดรายสัปดาห์อยู่ที่ 23 ล้านครั้ง
  • เมื่อสะสม minor version มา 24 รุ่น codebase ของ Zod 3 ก็มาถึงขีดจำกัด และฟีเจอร์กับการปรับปรุงที่ถูกขอมากที่สุดจำเป็นต้องมี breaking changes
  • Zod 4 ปิด public issue ที่ได้รับการโหวตมากที่สุด 9 จาก 10 รายการ

ประสิทธิภาพการ parse และประสิทธิภาพการคอมไพล์ TypeScript

  • Zod 4 มี benchmark ที่รันได้โดยตรงจาก repository
  • การปรับปรุงประสิทธิภาพการ parse:
    • การ parse สตริง: เร็วขึ้น 14 เท่า
    • การ parse array: เร็วขึ้น 7 เท่า
    • การ parse object: เร็วขึ้น 6.5 เท่า
  • benchmark การ parse object รัน Moltar validation library benchmark
  • อิงจาก tsc --extendedDiagnostics การคอมไพล์ไฟล์ง่าย ๆ มีจำนวน type instantiation ลดลงมาก
    • เมื่อใช้ "zod/v3": มากกว่า 25,000 ครั้ง
    • เมื่อใช้ "zod/v4": ประมาณ 175 ครั้ง
  • Zod 4 ออกแบบและทำให้ generics ของ ZodObject และ schema class อื่น ๆ เรียบง่ายขึ้น เพื่อหลีกเลี่ยง การพุ่งสูงของ instantiation
  • pattern ที่ chain .extend() และ .omit() ซ้ำ ๆ ใน Zod 3 ใช้เวลาคอมไพล์ 4000ms และการเรียก .extend() เพิ่มอาจทำให้เกิด error "Possibly infinite"
  • pattern เดียวกันใน Zod 4 คอมไพล์ได้ใน 400ms จึงเร็วขึ้น 10 เท่า
  • ในอนาคต หากใช้ร่วมกับ compiler tsgo ประสิทธิภาพใน editor ของ Zod 4 อาจขยายไปถึง schema และ codebase ที่ใหญ่ขึ้นได้

ขนาดบันเดิลและ Zod Mini

  • เปรียบเทียบขนาดบันเดิลหลักโดยบันเดิลสคริปต์ validation z.boolean() แบบง่ายด้วย rollup
  • ขนาดบันเดิลหลักแบบ gzip:
    • Zod 3: 12.47kb
    • Zod 4 เวอร์ชันปกติ: 5.36kb
  • บันเดิลหลักของ Zod 4 เวอร์ชันปกติเล็กกว่า Zod 3 ประมาณ 57% หรือลดลง 2.3 เท่า
  • API ของ Zod ที่เน้นเมธอดนั้นโดยพื้นฐานทำ tree-shaking ได้ยาก และแม้แต่สคริปต์ z.boolean() ง่าย ๆ ก็ยังดึง implementation ของเมธอดที่ไม่ได้ใช้ เช่น .optional(), .array() เข้ามาด้วย
  • Zod Mini มี API แบบ functional ที่สอดคล้องกับ zod แบบ 1:1 และสามารถ tree-shaking ได้
    • ในจุดที่ Zod ใช้เมธอด Zod Mini มักใช้ wrapper function
    • เมธอดสำหรับ parse ของ Zod และ Zod Mini เหมือนกัน
    • เพิ่ม refinement ด้วยเมธอด .check() อเนกประสงค์
  • ยังแนะนำให้ใช้ Zod ปกติต่อไปสำหรับ use case ส่วนใหญ่ ส่วนโปรเจกต์ที่มีข้อจำกัดขนาดบันเดิลเข้มงวดผิดปกติอาจพิจารณา Zod Mini
  • เมื่อใช้ zod/mini ขนาดบันเดิลแบบ gzip คือ 1.88kb
    • ลดลง 85% หรือ 6.6 เท่าเมื่อเทียบกับ Zod 3
    • ตารางเปรียบเทียบ: Zod 3 12.47kb, Zod 4 ปกติ 5.36kb, Zod 4 Mini 1.88kb
  • ดูรายละเอียดเพิ่มเติมได้ในเอกสาร zod/mini

Metadata และ JSON Schema

  • Zod 4 นำระบบใหม่สำหรับเพิ่ม metadata แบบ strongly typed ให้กับ schema
  • metadata ไม่ได้เก็บไว้ใน schema เอง แต่เก็บใน schema registry ที่เชื่อม schema กับ typed metadata
  • สร้าง registry ได้ด้วย z.registry() และลงทะเบียนแบบสะดวกผ่านเมธอด .register() ของ schema ได้ด้วย
  • Zod export global registry ชื่อ z.globalRegistry ที่รับ metadata ทั่วไปซึ่งเข้ากันได้กับ JSON Schema
  • เมธอด .meta() ช่วยเพิ่ม schema เข้า z.globalRegistry ได้สะดวก
  • เพื่อความเข้ากันได้กับ Zod 3 ยังมี .describe() อยู่ แต่ใน Zod 4 แนะนำให้ใช้ .meta()
  • Zod 4 มี การแปลงเป็น JSON Schema แบบ first-party ผ่าน z.toJSONSchema()
  • metadata ใน z.globalRegistry จะถูกรวมเข้าใน output ของ JSON Schema โดยอัตโนมัติ
  • ข้อมูลการปรับแต่ง JSON Schema ที่สร้างขึ้นอยู่ใน JSON Schema docs

การแสดง schema และความสามารถด้าน type ใหม่

  • Zod 4 สามารถอนุมาน recursive object type ได้อย่างเหมาะสม
    • สามารถแสดง recursive type และ mutually recursive type ได้
    • ไม่เหมือน pattern recursive type ของ Zod 3 ตรงที่ไม่ต้อง type casting
    • schema ที่ได้เป็น instance ของ ZodObject ปกติ และใช้เมธอดทั้งหมดได้
  • เพิ่ม File schema สำหรับ validate instance ของ File
  • z.templateLiteral() แสดง template literal type ของ TypeScript
    • type ของ Zod schema ที่แปลงเป็นสตริงได้จะเก็บ regular expression ภายใน
    • เป้าหมายรวมถึงสตริง, string format เช่น z.email(), number, boolean, bigint, enum, literal, undefined/optional, null/nullable และ template literal อื่น
    • constructor z.templateLiteral จะนำสิ่งเหล่านี้มาต่อกันเป็น super-regex เดียว
    • string format เช่น z.email() จะถูกบังคับใช้อย่างถูกต้อง แต่ custom refinement จะไม่ถูกบังคับใช้
    • รายละเอียดเพิ่มเติมอยู่ใน template literal docs
  • เพิ่ม number format ใหม่สำหรับแสดง integer แบบ fixed-width และ floating point type
    • คืน instance ของ ZodNumber ที่เพิ่ม constraint แบบ inclusive minimum/maximum ที่เหมาะสมไว้แล้ว
  • เพิ่ม bigint number format ที่เกินช่วงที่ JavaScript number แสดงได้อย่างปลอดภัย
    • คืน instance ของ ZodBigInt ที่เพิ่ม constraint แบบ inclusive minimum/maximum ที่เหมาะสมไว้แล้ว
  • ตอนนี้ z.literal() รับได้หลายค่าแบบเลือกได้

Error, internationalization และ string format

  • Zod 4 นำ locales API ใหม่มาใช้สำหรับแปล error message ทั่วทั้งระบบเป็นหลายภาษา
  • รายชื่อ locale ที่รองรับทั้งหมดอยู่ใน Customizing errors และจะอัปเดตเมื่อเพิ่มภาษาที่รองรับ
  • Zod implement function ระดับบนสุด z.prettifyError ที่แปลง ZodError เป็นสตริงในรูปแบบที่เป็นมิตรต่อผู้ใช้
    • ปัจจุบันรูปแบบยังตั้งค่าไม่ได้
    • อาจเปลี่ยนแปลงได้ในอนาคต
    • หากใช้งาน zod-validation-error อยู่แล้ว ก็ยังใช้ต่อได้
  • string format ทั้งหมด เช่น email ถูกยกระดับเป็น function ระดับบนสุดของโมดูล z
    • กระชับกว่าและเป็นมิตรต่อ tree-shaking มากกว่า
    • API แบบเมธอดที่สอดคล้องกัน เช่น z.string().email() ยังใช้งานได้ แต่ถูก deprecated แล้ว
    • มีกำหนดจะถูกลบใน major version ถัดไป
  • API z.email() รองรับ custom regular expression
    • ไม่มี canonical email regex ที่ตายตัวเพียงหนึ่งเดียว และแต่ละแอปพลิเคชันอาจเลือกเกณฑ์ที่เข้มงวดหรือผ่อนปรนต่างกัน
    • เพื่อความสะดวก Zod export regular expression ทั่วไปไว้หลายแบบ

การแปลง boolean และการทำ error customization ให้ง่ายขึ้น

  • z.coerce.boolean() เดิมเป็น API ง่าย ๆ ที่แปลงค่า falsy เป็น false และค่า truthy เป็น true
    • false, undefined, null, 0, "", NaN ฯลฯ จะกลายเป็น false
    • พฤติกรรมนี้สอดคล้องกับ API z.coerce อื่น ๆ
  • Zod 4 นำ z.stringbool() มาใช้เพื่อ boolean coercion แบบละเอียดขึ้นในสไตล์ env
    • สามารถปรับแต่งค่า truthy และ falsy ได้
    • รายละเอียดเพิ่มเติมอยู่ใน z.stringbool() docs
  • breaking changes ส่วนใหญ่ของ Zod 4 เกี่ยวข้องกับ API สำหรับปรับแต่ง error
  • การปรับแต่ง error ถูกรวมเป็นพารามิเตอร์ error เดียว
    • message ถูกแทนที่ด้วย error
    • พารามิเตอร์ message ยังรองรับอยู่ แต่ถูก deprecated แล้ว
    • invalid_type_error และ required_error ถูกแทนที่ด้วย error ในไวยากรณ์แบบ function
    • errorMap ก็ถูกแทนที่ด้วย error ในไวยากรณ์แบบ function เช่นกัน

Composability, refinement และ transformation

  • z.discriminatedUnion() รองรับ schema type หลายแบบที่ก่อนหน้านี้ไม่รองรับ
    • รวมถึง union และ pipe
    • สามารถใช้ discriminated union เป็น member ของ discriminated union อีกตัวได้ จึง ประกอบกันได้
  • ใน Zod 3 refinement ถูกเก็บไว้ใน class ZodEffects ที่ห่อ schema ต้นฉบับ
    • โครงสร้างนี้ทำให้ใช้ .refine() ร่วมกับเมธอด schema อื่น ๆ เช่น .min() ได้ไม่สะดวก
  • ใน Zod 4 refinement ถูกเก็บไว้ภายใน schema ทำให้ใช้ .refine() ร่วมกับเมธอด schema อื่น ๆ ได้อย่างเป็นธรรมชาติ
  • เมธอดใหม่ .overwrite() แสดง transform ที่ไม่เปลี่ยน inferred type
    • .transform() เป็น black box ที่ output type ไม่สามารถ introspect ได้ตอน runtime และ transform function อาจคืนค่าอะไรก็ได้
    • ด้วยเหตุนี้จึงไม่มีวิธีแปลงเป็น JSON Schema ได้อย่างปลอดภัย
    • .overwrite() คืน instance ของ class เดิม
    • overwrite function ถูกเก็บเป็น refinement และไม่แก้ไข inferred type
  • .trim(), .toLowerCase(), .toUpperCase() เดิมถูก implement ใหม่โดยใช้ .overwrite()

ฐานสำหรับผู้เขียนไลบรารี

  • การเพิ่ม Zod Mini ทำให้มี subpackage zod/v4/core ซึ่งรวมฟังก์ชันหลักที่ Zod และ Zod Mini ใช้ร่วมกัน
  • zod/v4/core ขยาย Zod จากไลบรารีธรรมดาให้เป็นฐาน validation ที่รวดเร็วและฝังใช้ในไลบรารีอื่นได้
  • หากสร้าง schema library สามารถอ้างอิง implementation ของ Zod และ Zod Mini แล้วสร้างบน zod/v4/core ได้
  • มีคู่มือ For library authors สำหรับผู้เขียนไลบรารี
    • ครอบคลุม best practice เมื่อสร้างบน Zod
    • ตอบคำถามที่พบบ่อยเกี่ยวกับวิธีรองรับ Zod 3, Zod 4 และ Mini พร้อมกัน

1 ความคิดเห็น

 
GN⁺ 2025-05-20
ความคิดเห็นจาก Hacker News
  • ผมเป็นผู้เขียนเอง ถามมาได้ทุกอย่างครับ ส่วนเรื่องนโยบายเวอร์ชัน ผมสรุปเหตุผลของแนวทางนี้ไว้อย่างค่อนข้างละเอียดที่นี่: https://github.com/colinhacks/zod/issues/4371
    ท้ายที่สุดแล้ว npm ไม่ได้ถูกออกแบบมาให้รับมือกับสถานการณ์แบบที่ Zod เจอได้ดี มีไลบรารีจำนวนหลายสิบถึงหลายร้อยตัวที่นำเข้าอินเทอร์เฟซ/คลาสของ Zod โดยตรงแล้วเอาไปใช้ใน API สาธารณะของตัวเอง ดังนั้นทุกครั้งที่ Zod ขึ้น major version ไลบรารีเหล่านั้นก็ต้องออก major version ใหม่ด้วย
    ถ้ามองแยกเดี่ยว ๆ ก็ดูสมเหตุสมผล แต่สำหรับ Zod มันอาจทำให้เกิด หิมะถล่มของเวอร์ชัน ที่เจ็บปวดสำหรับทุกคน และดูเหมือนว่าส่วนใหญ่ของอีโคซิสเต็มจะถูกตรึงอยู่กับ v3 ไปตลอด
    ดังนั้นจึงเลือกแนวทางคล้ายกับ Go คือแทนที่แพ็กเกจจะออกเวอร์ชันใหม่ที่ทำให้ความเข้ากันได้พัง ก็เพิ่ม subpath ใหม่แทน ในอีโคซิสเต็ม TypeScript ไลบรารีสามารถมี peer dependency เพียงตัวเดียวคือ zod@^3.25.0 แล้วนำเข้าสิ่งที่ต้องใช้จาก "zod/v3" และ "zod/v4" เพื่อรองรับทั้งสองเวอร์ชันพร้อมกันได้

    • การที่ tsc มีประสิทธิภาพดีขึ้น ในโค้ดเบสขนาดใหญ่นี่น่ายินดีเป็นพิเศษ และการเปลี่ยนแปลงเรื่อง discriminated union ก็น่าจะช่วยได้มากในบางสถานการณ์ที่ก่อนหน้านี้ยังขาดอยู่
      แต่ถึงจะเข้าใจเหตุผลที่เลือกนโยบายเวอร์ชันที่ค่อนข้างแปลกเพราะมีเงื่อนไขพิเศษ ผมก็ยังอยากให้มีแพ็กเกจ 4.0.0 ด้วย สำหรับทีมที่ไม่ต้องกังวลเรื่องเวอร์ชัน Zod ชนกันใน transitive dependency
      ถ้าผมเข้าใจถูก ต้องเปลี่ยน import เป็น "zod/v4" ซึ่งเป็นการเปลี่ยนที่มีเสียงรบกวนสูงมาก และอาจปวดหัวเพราะต้องใช้กฎ lint มาจับปัญหาที่ IDE auto import จาก 'zod'
    • วิธีนี้สมเหตุสมผลมาก หมายความว่า อัปเดตความปลอดภัยของเวอร์ชันเก่า ก็สามารถถูกส่งมาพร้อมกันใน release จากโค้ดเบสเดียวกันได้
    • ผมดูบนมือถือเลยไม่เห็นว่าในบทความมีพูดไว้แล้วหรือเปล่า แต่อยากรู้ว่างานแก้ .optional() ใน TypeScript รวมอยู่ในการแก้ issue 9–10 อันดับแรกหรือไม่: https://github.com/colinhacks/zod/issues/635
      นี่เป็นความเจ็บปวดที่ใหญ่ที่สุดของผมใน Zod แต่ Zod ดีมากจนยังยอมรับและใช้งานต่ออยู่
    • ฟีเจอร์ใหม่น่าจะช่วยให้ผมลบ hack ชั่วคราวที่ทำไว้ในเครื่องออกได้เยอะ ตอนนี้ผมใช้เวอร์ชันของตัวเองที่อิงจาก https://github.com/raflymln/zod-key-parser เพื่อเลี่ยงการพิมพ์ชื่อฟอร์มผิด เลยแปลกใจที่ฟีเจอร์แบบนี้ไม่ได้ถูกใส่เข้าไปในไลบรารีโดยตรง
      อยากรู้ว่ามองว่านี่อยู่นอกขอบเขตของ Zod หรือแค่ยังไม่มีเวลานำไป implement การอภิปรายที่เกี่ยวข้องอยู่ที่ https://github.com/colinhacks/zod/discussions/2134
    • ถ้าหมายความว่าเวอร์ชันแรกสุดของ Zod 4 คือ 3.25.0 แบบนี้ฟังดูไม่ค่อยสมเหตุสมผลเท่าไร
      ผมใช้ Zod alpha มาหลายเดือนแล้ว และแค่อยากแก้ package.json เพื่ออัปเกรด แต่ตอนนี้ดูเหมือนต้องไล่ค้น git history ทั้งหมด
      ผมซาบซึ้งกับโปรเจกต์นี้มาก แต่ feedback ข้อนี้ดูเหมือนถูกทำให้ยากเกินจำเป็น น่าจะปล่อย 4.x ควบคู่กับ 3.x ได้หรือเปล่า
  • ข้อความที่ว่า “เพื่อทำให้การ migrate ง่ายขึ้น จะเผยแพร่ Zod 4 เป็นส่วนหนึ่งของ release zod@3.25 คู่กับ Zod 3 และให้นำเข้าจาก subpath "/v4"” ดูเหมือนเป็นสัญญาณว่า การจัดการ dependency ของ npm เละเทะไปหมด
    peer dependency พังจนทำให้ v4 ต้องแสร้งทำเป็น v3

    • ผมเป็นผู้เขียนเอง เหตุผลของแนวทางนี้เขียนไว้อย่างค่อนข้างละเอียดที่นี่: https://github.com/colinhacks/zod/issues/4371
      เห็นด้วยว่า npm จัดการสถานการณ์ที่ Zod เจอได้ไม่ดีนัก แต่ Zod อยู่ภายใต้ข้อจำกัดที่แทบไม่มีไลบรารีไหนเจอ มีไลบรารีจำนวนหลายสิบถึงหลายร้อยตัวที่นำเข้าอินเทอร์เฟซ/คลาสจาก "zod" โดยตรงแล้วใช้ใน API สาธารณะของตัวเอง
      ไลบรารีแบบนี้ต้องออก major version ใหม่ทุกครั้งที่ Zod ขึ้น major version และสำหรับ Zod สิ่งนั้นอาจก่อให้เกิด หิมะถล่มของเวอร์ชัน ที่เจ็บปวดสำหรับทุกคน พูดตรง ๆ ผมคิดว่าส่วนใหญ่ของอีโคซิสเต็มน่าจะถูกตรึงไว้กับ v3 ไปตลอด
      ดังนั้นจึงเลือกวิธีคล้ายกับ Go คือไม่ออกแพ็กเกจเวอร์ชันใหม่ทุกครั้งที่มี release ที่ทำให้ความเข้ากันได้พัง แต่เพิ่ม subpath ใหม่แทน ในอีโคซิสเต็ม TypeScript สามารถรองรับ "zod/v3" และ "zod/v4" พร้อมกันได้ด้วย peer dependency ตัวเดียวคือ zod@^3.25.0
    • ข้อสรุปที่ว่า “peer dependency พังจน v4 ต้องแสร้งทำเป็น v3” ดูไม่น่าถูกต้อง ผมมองว่า Zod v4 ถูกใส่ไว้ใน v3 เพื่อให้ผู้ใช้ค่อย ๆ ย้ายได้
      กล่าวคือ refactor จุดใช้งานทีละจุดให้เป็น import ... from 'zod/v4' แล้วพอเสร็จก็อัปขึ้น v4 อย่างเต็มตัว
    • รู้สึกว่าอะไรก็ตามที่พูดในแง่ลบจะได้รับ upvote ไปหมด การตัดสินใจนี้ไม่ใช่ข้อจำกัดโดยเนื้อแท้ของ npm เท่าไร แต่ใกล้เคียงกับวิธีเชิงปฏิบัติที่ช่วยให้ไลบรารีจำนวนมากที่มี breaking changes ค่อย ๆ เปลี่ยนผ่านได้มากกว่า
    • อาจเป็นเพราะใช้แต่ npm มาหลายปีจนมุมมองแคบลงก็ได้ แต่สงสัยว่ามีวิธีที่ดีกว่านี้อย่างไรในการ migrate แบบทำซ้ำเป็นรอบ ๆ แทนที่จะย้ายจาก v3 ไป v4 ครั้งใหญ่ในทีเดียว
    • นี่ไม่ใช่ปัญหาของ npm อย่างเดียว ปัญหาที่ dependency บางตัวไปพึ่งพาเวอร์ชันเก่าผ่าน transitive dependency อื่นนั้นเกิดขึ้นแทบทุกอีโคซิสเต็ม
      ตรงกันข้าม npm ยังมีทางออกมากกว่า เพราะถ้าจำเป็นก็สามารถรันหลายเวอร์ชันพร้อมกัน หรือเลือก override บางส่วนของกราฟ transitive dependency ได้
      ระบบจัดการแพ็กเกจไหนแก้ปัญหานี้ได้บ้าง? แม้แต่แพลตฟอร์มที่ “เสถียร” อย่าง Maven ก็จัดการปัญหาแบบนี้ด้วยการออกเวอร์ชันใน namespace ใหม่ เช่นตอน Apache Commons ย้ายจาก v2 ไป v3
  • มีคำถามที่สงสัยมานาน เคยได้ยินว่า Zod อาจเหมาะกับกรณีนี้ แต่พอดูเอกสารแล้วก็ยังไม่ค่อยรู้ว่าควรทำอย่างไร
    สมมติว่า API ฝั่งเซิร์ฟเวอร์คืนค่าชนิดที่ละเอียดกว่าสิ่งที่มันสามารถแสดงออกได้ เช่น api/:postid/author คืนค่า User แต่ User นั้นอาจเป็น User ปกติหรือ User นิรนามก็ได้ ทำให้ฟิลด์อย่าง username, location อาจมาเป็น null ในกรณีนี้เราอาจอยากแสดงอ็อบเจ็กต์ User เป็น discriminated union
    อ็อบเจ็กต์ที่มาจาก endpoint อื่นก็อาจต้องมีการแปลงชนิดเช่นกัน User บางครั้งอาจมี Post[] รวมอยู่ และถ้า Post เป็นโพสต์ของ moderator ก็อาจมีพร็อพเพอร์ตีเฉพาะได้
    เมื่อก่อนเคยเขียนฟังก์ชันอย่าง normalizeUser() กับ normalizePost() แต่ไม่นานก็เละเทะ เพราะแต่ละ endpoint คืนค่าสับเซ็ตที่ต่างกันของโมเดล User/Post จนต้องสร้าง normalizePost ประมาณ 5 ตัว ซึ่งวุ่นวายมาก อยากรู้ว่าปกติปัญหาแบบนี้แก้กันอย่างไร

    • น่าจะใช้ discriminated union ได้
      เช่น แยกอ็อบเจ็กต์ success กับ failed ตามฟิลด์ที่ใช้แยกแยะอย่าง status ถ้ามีพร็อพเพอร์ตีพิเศษสำหรับ moderator เยอะ แต่ไม่อยากไล่ระบุหรือตรวจทั้งหมด ก็สามารถกำหนดพฤติกรรมแบบ pass-through ได้
      ต่อให้หลายเมธอดมี schema ต่างกัน ถ้ามีส่วนร่วมกัน ก็สร้าง object schema ที่ใช้ร่วมกัน แล้วสร้างอ็อบเจ็กต์อื่น ๆ ที่เพิ่มฟิลด์เข้าไปได้ ถ้าความเป็นไปได้มีเยอะก็ยังอาจรกอยู่ดี แต่ถ้าสถานการณ์เดิมก็รกอยู่แล้ว อย่างน้อย validator ก็ช่วยตรวจความรกนั้นให้ได้
    • ในอุดมคติควรมี single source of truth สำหรับว่า User อาจมีรูปแบบใดได้บ้าง ซึ่งอาจเป็น discriminated union อย่าง User กับ AnonymousUser
      ถ้าไม่ใช่ full-stack TypeScript เช่นแบ็กเอนด์ Python ก็อาจนิยามรูปแบบ User หลายแบบด้วยโมเดลและ union ของ Pydantic แล้วสร้างสคีมา OpenAPI/GraphQL พร้อมทำ code generation เพื่อสร้าง TypeScript client type
    • ต้องรู้ use case เพิ่มถึงจะแก้ได้ แต่ถ้าเพิ่ม พร็อพเพอร์ตีสำหรับแยกแยะ อย่าง "user_type" ให้กับทุกชนิดใน union ก็จะจัดการกรณีทั่วไปและกรณีพิเศษได้ง่ายขึ้น
      เช่น ถ้าตรวจ user.user_type === 'authenticated' ระบบชนิดก็จะรู้ว่าหลังจากนั้นสามารถใช้ user.name ได้
    • ไม่ได้ช่วยโดยตรงกับกรณีนี้ แต่ GraphQL ถูกสร้างมาเพื่อเรื่องแบบนี้
      ไลบรารี TypeScript สำหรับ query ของ GraphQL สามารถอนุมานรูปแบบ response แบบไดนามิกจากฟิลด์ที่เลือกได้ ถ้าเลือก { user { username posts { text } } } ก็จะได้ชนิด response ที่มี posts และถ้าเลือกแค่ { user { username } } พร็อพเพอร์ตี posts ก็จะหายไปโดยสิ้นเชิง
    • เซิร์ฟเวอร์ควรส่งออก type มา การเขียน type ด้วยมือบน client นั้นไม่สมเหตุสมผล
      เซิร์ฟเวอร์รู้ข้อมูลที่ตัวเองส่งอยู่แล้ว จึงควรให้ข้อมูลนั้นกับ client ในรูป metadata ในเชิงปฏิบัติ แบ็กเอนด์ Python อาจใช้สคีมา Pydantic แล้วสร้างสเปก OpenAPI อัตโนมัติจากสิ่งนั้น จากนั้นให้ client สร้าง type ขึ้นมา
  • Zod ดีกว่าทางเลือกอื่น ๆ ที่เคยเห็นมาก แต่บางครั้งข้อเท็จจริงที่ว่าเราจำเป็นต้องมี การตรวจสอบแบบ explicit แบบนี้เอง ก็ให้ความรู้สึกเหมือนเป็นความล้มเหลวของพัฒนาเว็บสมัยใหม่ว่ามาถึงจุดไหนแล้ว
    ในการพัฒนาแบบ full-stack มันน่าหงุดหงิดมากที่ต้องอธิบายรูปทรงเดียวกันหลายวิธี ต้องมีการตรวจ input ใน JS, Swagger สำหรับนิยาม API, การตรวจ input ฝั่งเซิร์ฟเวอร์, ORM เพื่อให้สอดคล้องกับ schema และยังต้องมีนิยาม TypeScript แยกกันระหว่างเซิร์ฟเวอร์กับ client อีก น่าเบื่อมาก

    • การที่ TypeScript ยืนกรานจะเป็นระบบสำหรับการตรวจแบบ static/compile-time เท่านั้น ถือเป็นบาดแผลที่น่าเสียดายของทั้ง ecosystem ไม่ได้อยากให้ TypeScript กลายเป็น runtime checker แต่หวังว่ามันจะส่งออกข้อมูล type ที่ใช้ประโยชน์กับ class, function และ object ได้
      TypeScript ให้ความรู้สึกเหมือนเป็น single source of truth ที่ดีที่สุดที่เรามี แต่ในความเป็นจริง เครื่องมือที่พยายามทำ reflection ต่างก็นิยามโมเดลและ builder ของตัวเองซ้ำขึ้นมาใหม่ มีโดเมนใหญ่ ๆ ที่ reflection เป็นแกนหลักอยู่ถึง 5 แห่ง และแต่ละแห่งก็มี implementation หลายตัว
      เครื่องมือปล่อย type ของ TypeScript แบบเก่าอย่าง reflect-metadata ให้ข้อมูล runtime type ได้บางส่วน แต่เล็งไปที่ decorator และสเปก metadata ที่เก่ามาก: https://www.npmjs.com/package/reflect-metadata
      บางทีเราอาจเพิ่งอยู่ช่วงเริ่มต้นของการรวมศูนย์ก็ได้ โปรเจกต์ Standard Schema ดูเหมือนจะได้รับการสนับสนุนจากไลบรารีตรวจสอบหลัก ๆ ส่วนใหญ่ แต่การขยายไปถึงเครื่องมือนิยาม API หรือ ORM ยังอยู่ในระยะเริ่มต้นมาก
      https://github.com/standard-schema/standard-schema?tab=readm...
    • จริง ๆ แล้วนี่แหละคือแก่นของเครื่องมือแบบนี้ นิยามครั้งเดียว แล้วสร้างทุกอย่างที่อยู่ข้างใต้แบบไดนามิก
      เปลี่ยนใน Zod schema ครั้งเดียว ก็แพร่ไปทั่วทั้งแอปพร้อมกับการตรวจ type เท่ากับว่า Zod schema กลายเป็น single source of truth
    • โดยทั่วไปไม่จำเป็นต้องนิยามข้อมูลซ้ำไปซ้ำมาหลายที่ มีเครื่องมือที่แปลงเป็น Zod หรือแปลงจาก Zod เยอะมาก
      ถ้ามี JSON Schema/Swagger schema อยู่แล้ว ก็สร้าง Zod จากตรงนั้นได้ ถ้าใช้ TypeScript ORM ก็แทบจะแน่นอนว่ามี Zod generator
      พูดตรง ๆ คือ Zod ถูกใช้กว้างขวางมากจนสำหรับผม มันกลายเป็น นิยาม schema แบบรวมศูนย์ ที่ส่วนอื่น ๆ ของ stack พึ่งพาได้ เพราะนิยาม type โดยตรง นักพัฒนาจึงรักษาให้เป็นปัจจุบันจริง ๆ เอกสาร Swagger ของบริษัทมักตามหลังการเปลี่ยนแปลงเสมอ
    • API ทุกตัวล้วนอยู่ในสถานะทดลองและเปลี่ยนแปลง/วิวัฒนาการต่อเนื่อง เว็บก็เช่นกัน
      กรณีที่การคงสภาพเดิมพอใช้ได้ ก็มีแค่เวลารับมือกับ client และนายจ้างที่ต้องการแค่ผลลัพธ์ ไม่สนใจวิธีทำเท่านั้น ในบริบทแบบนั้น ชั้นความซับซ้อนที่น่าเศร้าเหล่านี้อย่างน้อยก็ช่วยเพิ่มชั่วโมงที่เรียกเก็บเงินได้
    • คนจำนวนมากที่บ่นว่าการพัฒนาเว็บสมัยใหม่ซับซ้อน ก็น่าจะขยาดเหมือนกันถ้ามีข้อเสนอให้แทนที่ทั้งหมดนี้ด้วย TypeScript กับ Zod เฉย ๆ
      แน่นอนว่าอยู่บนสมมติฐานว่า Zod คือไลบรารีนิยาม schema และตรวจสอบของ TypeScript ที่ตนเลือกใช้
  • Zod 4 ดูน่าสนใจ แต่ถึงจะรวมการปรับปรุงล่าสุดแล้ว ArkType ก็ยังเร็วกว่าอยู่หลายเท่าตัวในระดับเลขหลักเดียว
    บางครั้งเพื่อรักษาความเข้ากันได้ย้อนหลังและความเข้ากันได้ของไวยากรณ์ ก็ทำให้ยากที่จะทำให้เร็วได้เท่ากับไลบรารีที่เริ่มใหม่ทั้งหมด ผมเพิ่งวิเคราะห์เครื่องมือเหล่านี้ทั้งหมดในโปรเจกต์ล่าสุด และเลือก ArkType ส่วนหนึ่งก็เพราะเหตุผลนี้ อีกอย่างคือรู้สึกว่าประสบการณ์การใช้งานกับ TypeScript ดีกว่าด้วย

    • ผมดูตัวชี้วัดความเร็วแล้ว แต่สงสัยว่าช่วยอธิบายได้ไหมว่าความเร็วนั้นสร้างความแตกต่างตรงไหน
      เราใช้ Zod แค่ตรวจสอบความถูกต้องของฟอร์ม เลยคิดว่า “ทำไมเรื่องนี้ถึงสำคัญ?” สงสัยว่ามีกรณีที่ผู้คนตรวจสอบความถูกต้องของข้อความอินพุต API ที่มี throughput สูง ๆ จนประสิทธิภาพสำคัญขึ้นหรือเปล่า
    • อยากรู้ว่าทำไมถึงเลือก ArkType แทน TypeBox
    • น่าสนใจว่าตอนที่ค้นคว้า ผมไม่ได้เห็นเครื่องมือนั้นเลย ผมเองก็ดูเรื่องประสบการณ์การใช้งานกับ TypeScript เป็นพิเศษเหมือนกัน
      ถึงอย่างนั้นก็คงไม่ย้ายออกจาก Zod
    • ArkType เขียนยากเหมือนฝันร้าย ส่วน Zod เขียนสบาย
  • ขอแสดงความยินดีกับทีม Zod สำหรับรีลีสใหม่ แม้จะเสี่ยงฟังดูแง่ลบเกินไป แต่จำนวน การเปลี่ยนแปลงที่ทำให้เข้ากันไม่ได้ ในคู่มือไมเกรชันทำให้รู้สึกขนลุก
    ถ้าเป็นโปรเจกต์ที่พึ่งพา Zod มาก น่าจะเป็นงานหนักที่ต้องใช้สมาธิและเวลาของนักพัฒนาเยอะมาก ผมยิ่งเข้าใจเป็นพิเศษเพราะเคยดูแลโปรเจกต์ฟรอนต์เอนด์อายุ 4–5 ปีอยู่หลายตัวในที่ทำงาน
    โปรเจกต์ React ขนาดใหญ่มักพึ่งพาไลบรารีจำนวนมาก และเมื่อแต่ละไลบรารีออกการเปลี่ยนแปลงใหญ่ ๆ แถมเอกสารยังไม่พอ ก็กลายเป็นเรื่องท่วมท้นได้อย่างรวดเร็ว นี่เป็นหนึ่งในส่วนที่ผมเกลียดที่สุดเวลาทำงานกับ JavaScript และการทำให้ทุกอย่างเข้ากันและยังทำงานลื่นไหลต่อไปได้รู้สึกเหมือนการเดินขึ้นเขาที่ไม่มีวันจบ

    • เห็นด้วยเต็มที่ เราดูแลแอป Next.js ขนาดใหญ่อยู่หลายตัว และในช่วง 1 ปีที่ผ่านมา ต้องเจอกับการเปลี่ยนแปลง cache ครั้งใหญ่จาก Next.js 14 → 15, Next.js pages → app router, React 18 → 19, Eslint 8 → 9, Tailwind 3 → 4
      พูดตรง ๆ คือเหมือนฝันร้าย และคิดเลยว่าน่าจะสร้างด้วย Django ไปดีกว่า ผิดคาดมากที่การไมเกรต Tailwind 3 → 4 เป็นหนึ่งในส่วนที่เจ็บปวดที่สุด
    • นั่นแหละคือเหตุผลที่พวกเขาเลือกใช้ วิธีแจกจ่ายแบบคู่ พร้อมกับเอดิชัน mini แยกต่างหาก ทำให้ไมเกรตแบบค่อยเป็นค่อยไปได้ง่ายโดยไม่ต้องไปยุ่งกับ package manager
      ผู้ใช้ที่ไม่สนใจเอดิชัน mini ก็ไม่จำเป็นต้องสนใจส่วนนั้น แต่ประโยชน์ด้าน tree shaking ของเอดิชัน mini ใหญ่เกินไปจนกระตุ้นให้เกิดการพัฒนาทางเลือกอื่น ๆ และ Zod ก็ต้องยอมรับมัน ไม่อย่างนั้นก็เสื่อมความนิยมลง
    • ก็ใช้ LLM ไปสิ
  • ในฐานะนักพัฒนา full-stack ที่ดูแล SaaS ในโปรดักชันเองและมีผู้ใช้หลายพันคน บางครั้งผมรู้สึกดีที่ได้ตระหนักว่ามีปัญหาอีกมากแค่ไหนที่ผมไม่รู้เลย เพราะตัดสินใจไม่สร้าง SPA และไม่ใช้เฟรมเวิร์กฟรอนต์เอนด์ JS
    ผมไม่เคยแม้แต่คิดว่าต้องมีอะไรอย่าง Zod/ArkType เพิ่งเคยได้ยินครั้งแรก และก็ไม่มีเหตุให้ต้องใช้
    ผมทึ่งกับปริมาณความพยายาม ความซับซ้อน และเครื่องมือที่ต้องใช้ในการสร้าง SPA ถ้าตัดสินใจไม่สร้าง SPA ปัญหาทั้งกลุ่มหนึ่งก็หายไปเลย แทนที่จะทำอย่างนั้น ก็ใช้เบราว์เซอร์ตามที่มันถูกออกแบบมาแต่เดิม คือรีเฟรชทั้งหน้าและเน้นพัฒนาแบ็กเอนด์ แล้วค่อยเพิ่มความเป็น reactive เป็นครั้งคราวด้วยเฟรมเวิร์กฝั่งแบ็กเอนด์ล้วน ๆ อย่าง Laravel Livewire
    ทุกอย่างตั้งแต่การควบคุมการเข้าถึง การตรวจสอบความถูกต้อง ไปจนถึงการจัดการ state ล้วนเรียบง่าย แอปเร็ว ตอบสนองดี ทันสมัย เป็นมิตรกับ SEO และกำลังให้บริการผู้ใช้หลายพันคนในโปรดักชัน

    • ในโปรเจกต์ใหญ่สองโปรเจกต์ที่มีทีมทำงานร่วมกันมากกว่า 5 ทีม เราใช้ Zod และ Protobuf เพื่อให้ schema evolution เป็นไปได้ดี แม้จะเป็นฟรอนต์เอนด์เบา ๆ การมีอะไรสักอย่างไว้ตรวจสอบ schema ที่แบ็กเอนด์ก็ยังมีความหมาย
      สุดท้ายแล้ว schema ก็ต้องอยู่ที่ไหนสักแห่ง อาจนิยามอยู่ในระบบฐานข้อมูล หรือในโมเดล ORM ก็ได้ แต่ในโปรเจกต์ใหญ่ การวาง schema ไว้ในระบบฐานข้อมูลหรือ ORM อาจก่อปัญหาที่ยากมากได้ เราจึงใช้ Zod และ Protobuf ผมมองว่า Protobuf เองก็สามารถถูกแทนที่ทั้งหมดด้วย Zod หรือสิ่งคล้าย ๆ กันได้
      คดีอื้อฉาวของ Post Office ในสหราชอาณาจักรเกี่ยวข้องกับระบบ IT ชื่อ Horizon: https://en.wikipedia.org/wiki/British_Post_Office_scandal
      หลังอ่านรายละเอียดแล้ว ผมคิดว่าถ้ามี การตรวจสอบ schema แบบ Zod ก็น่าจะมีส่วนช่วยป้องกันคดีอื้อฉาวนี้ได้ ภาคผนวกทางเทคนิคของเอกสารศาลในคดีแบบกลุ่มก็กล่าวถึงการไม่มีการใช้ schema เช่นกัน: https://en.wikipedia.org/wiki/Bates_%26_Others_v_Post_Office...
    • Zod ไม่ได้เกี่ยวกับ SPA และไม่ใช่เครื่องมือสำหรับฟรอนต์เอนด์เท่านั้น คุณไม่ได้ตรวจสอบความถูกต้องของอินพุตในโค้ดเหรอ?
    • ถ้าหลับตา ปัญหาทั้งกลุ่มก็อาจกลายเป็นไม่มีอยู่จริงได้เหมือนกัน
    • อยากรู้ว่าคุณทำการตรวจสอบความถูกต้องฝั่งเซิร์ฟเวอร์อย่างไร
      ตอนนี้ฟังดูเหมือนเขียน if แบบ defensive จำนวนมาก หรือแย่กว่านั้นคืออาจพึ่งพาแค่การตรวจสอบฟอร์มฝั่งไคลเอนต์ด้วย HTML
    • ฝั่งเรามี Zod schema สำหรับแบ็กเอนด์ล้วน ๆ จำนวนมากที่ใช้เป็นส่วนหนึ่งของ data pipeline ดังนั้นจึงดูไม่เหมือนเครื่องมือที่มีอยู่เพื่อแก้ปัญหา SPA เท่านั้น
  • ไม่ใช่ผู้เชี่ยวชาญจึงต้องพูดอย่างระมัดระวัง แต่คิดว่า JSON Schema อาจเป็นตัวเลือกที่ดี เพราะเป็นแบบอิงสคีมา จึงสามารถทำตัวตรวจสอบความถูกต้องในภาษาอื่นที่ไม่ใช่ TypeScript ได้
    https://ajv.js.org เป็นหนึ่งในไลบรารี JSON Schema แบบนั้น อยากรู้ว่าเมื่อเทียบกับ Zod แล้วเป็นอย่างไร

    • Zod ไม่ใช่เครื่องมือสำหรับตรวจสอบ JSON เท่านั้น สามารถตรวจสอบออบเจกต์ที่แสดงเป็น JSON ไม่ได้หากไม่มีการแปลง เช่น วันที่หรืออินสแตนซ์ของคลาส
      ถ้าต้องการก็ใช้เป็นตัวแปลง JSON ได้ด้วย เช่น เขียนสคีมาที่รับสตริง ตรวจสอบว่าเป็นสตริงวันที่ ISO หรือไม่ แล้วส่งออกเป็นออบเจกต์ Date ได้
    • Zod 4 รองรับการแปลงสคีมาของ Zod เป็น JSON Schema แบบเนทีฟ ก่อนหน้านี้ก็ทำได้ผ่านไลบรารีของบุคคลที่สาม
      ความแตกต่างสำคัญคือการประมวลผลล่วงหน้าและ refine ใน Zod สามารถให้คอลแบ็กก่อนการตรวจสอบได้ ซึ่งมีประโยชน์มาก แต่ไม่สามารถแสดงเป็น JSON ได้ ตัวอย่างเช่น การแปลง MM/DD/YYYY เป็น DD/MM/YYYY ก่อนตรวจสอบวันที่ เป็นสิ่งที่มีประโยชน์บ่อยกว่าที่คิด
    • ในกรณีแบบนั้น JSON Schema เป็นตัวเลือกที่ดี และ TypeBox ก็เป็นวิธีที่ใช้สร้างมันได้ค่อนข้างดี
      สำหรับการตรวจสอบจะใช้ AJV หรือระบบตรวจสอบสคีมาของ TypeBox เองก็ได้ อย่างหลังเร็วกว่าอย่างมาก แต่รองรับเฉพาะแบบซิงโครนัส ส่วน AJV มีตัวเลือกตรวจสอบแบบอะซิงโครนัส เช่น การตรวจสอบกับฐานข้อมูล
    • สำหรับการตรวจสอบข้ามภาษา JSON Schema คือทางที่เหมาะสม หากครอบด้วย OpenAPI ก็จะได้เอกสาร API ที่ดีพอใช้มาฟรี ๆ ด้วย
  • แปลกใจกับปฏิกิริยาเชิงลบรอบ ๆ ประเด็นนี้
    เคยทดสอบ Zod v4 เวอร์ชันแรก ๆ และชอบ API ใหม่ แต่กังวลมากว่าเส้นทางการย้ายเวอร์ชันจะเป็นอย่างไร ถึงขั้นคิดจะเสนอให้เผยแพร่ด้วยชื่อแพ็กเกจใหม่ไปเลย
    แต่แนวทางของผู้เขียนนั้นฉลาด ทำให้คนอย่างผมสามารถ เริ่มใช้ v4 ได้ทันที โดยไม่ต้องรอให้ dependency ทั้งหมดอัปเดตก่อน

    • เป็นแนวทางที่ดี เรื่องอย่างการตรวจสอบความถูกต้องไม่มีทางรับมือกับการย้ายแบบทั้งหมดหรือไม่ก็ไม่ทำเลยได้
  • เพิ่งเริ่มนำ Zod มาใช้ในโปรเจกต์ใหม่ และไม่มีจังหวะไหนจะดีไปกว่านี้แล้ว
    จากสิ่งที่เห็นตอนนี้ ถ้าจะย้ายไป v4 คงมีสิ่งที่ต้องแก้เยอะมากจริง ๆ