Typing APIs & Boundaries
ทุก bug เข้ามาที่ boundary
หัวข้อที่มีชื่อว่า “ทุก bug เข้ามาที่ boundary”ภายใน code ของคุณเอง compiler คอยดูแลให้ bug แอบเข้ามาตรงที่ typed code เจอกับโลกที่ไม่มี type: response จาก network, localStorage, form, third-party library, JSON.parse ตรง edge เหล่านั้น การการันตีของ TypeScript ดีได้แค่เท่าที่คุณจัดการกับข้อมูลที่เข้ามา
การตัดสินใจแรกคือจะเลือก “ไม่รู้ type นี้” แบบไหน: any หรือ unknown
any ปิด checker ส่วน unknown เก็บ checker ไว้
หัวข้อที่มีชื่อว่า “any ปิด checker ส่วน unknown เก็บ checker ไว้”const a: any = JSON.parse(input);a.user.name.toUpperCase(); // compile ผ่าน — และอาจระเบิดตอน runtime
const u: unknown = JSON.parse(input);u.user.name; // ❌ Error: 'u' is of type 'unknown'.any opts out จากการ type check — เข้าถึงอะไรก็ได้ และการการันตีทั้งหมดถูกปิด unknown คือ top type ที่ซื่อสัตย์: รับ value อะไรก็ได้ แต่ห้ามคุณทำอะไรกับ value นั้นจนกว่าจะ พิสูจน์ ว่าเป็นอะไร เลือก unknown ที่ทุก boundary และมอง any เป็น code smell ที่คุณกำลังกำจัดออกไป
สังเกตว่า JSON.parse ถูก type ให้ return any — ผลลัพธ์จึงแอบวางยา downstream ทั้งหมด เว้นแต่คุณจะ assign เป็น unknown ทันทีแล้ว validate
เปลี่ยน unknown ให้เป็น type ที่เชื่อถือได้
หัวข้อที่มีชื่อว่า “เปลี่ยน unknown ให้เป็น type ที่เชื่อถือได้”การ narrow unknown ต้องใช้ runtime check ที่ compiler เข้าใจ สำหรับ shape ง่าย ๆ ใช้ type guard ที่เขียนเอง:
type User = { id: number; name: string };
function isUser(v: unknown): v is User { return ( typeof v === "object" && v !== null && "id" in v && typeof (v as any).id === "number" && "name" in v && typeof (v as any).name === "string" );}
const data: unknown = JSON.parse(input);if (isUser(data)) { data.name.toUpperCase(); // ✅ data เป็น User ตรงนี้}พอเป็นของจริง type guard ที่เขียนเองเริ่มน่าเบื่อและ drift ออกจาก type คำตอบมาตรฐานคือ schema/parser library (zod, valibot และเพื่อน ๆ) ที่ declaration เดียวเป็น ทั้ง runtime validator และ static type:
import { z } from "zod";
const User = z.object({ id: z.number(), name: z.string() });type User = z.infer<typeof User>; // static type ที่ derive จาก schema
const data = User.parse(JSON.parse(input));// data ถูก type เป็น User และถูก check ตอน runtime — throw ถ้า invalidsource of truth เดียว, check ตอน runtime, infer ตอน compile time นี่คือ boundary ที่ทำถูก
flowchart LR raw["unknown (JSON, network, storage)"] -->|schema.parse / type guard| typed["trusted type (User)"] typed --> use["use freely no more checks needed"]
Branded type สำหรับ nominal ID
หัวข้อที่มีชื่อว่า “Branded type สำหรับ nominal ID”structural typing หมายความว่า UserId กับ OrderId ที่เป็น string ทั้งคู่ใช้แทนกันได้ — คุณจึงส่งตัวหนึ่งไปที่ที่คาดหวังอีกตัวได้ เมื่อเรื่องนี้สำคัญ ให้ brand type เหล่านั้นด้วย marker แบบ phantom:
type UserId = string & { readonly __brand: "UserId" };
function getUser(id: UserId) { /* ... */ }
const raw = "u_123";getUser(raw); // ❌ string ธรรมดาไม่ใช่ UserIdgetUser(raw as UserId); // ✅ คุณ assert มันอย่างตั้งใจตรง boundarybrand มีอยู่แค่ตอน compile time (ถูก erase ทิ้ง), ไม่มี cost ตอน runtime และบังคับให้ทุก UserId ถูก สร้างอย่างตั้งใจ — โดยปกติคือทันทีหลัง validate นี่คือวิธีได้ nominal safety ในระบบที่เป็น structural