ข้ามไปยังเนื้อหา

Typing APIs & Boundaries

ภายใน code ของคุณเอง compiler คอยดูแลให้ bug แอบเข้ามาตรงที่ typed code เจอกับโลกที่ไม่มี type: response จาก network, localStorage, form, third-party library, JSON.parse ตรง edge เหล่านั้น การการันตีของ TypeScript ดีได้แค่เท่าที่คุณจัดการกับข้อมูลที่เข้ามา

การตัดสินใจแรกคือจะเลือก “ไม่รู้ type นี้” แบบไหน: any หรือ unknown

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

การ 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 ถ้า invalid

source 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"]
validate ที่ edge, เชื่อภายใน

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 ธรรมดาไม่ใช่ UserId
getUser(raw as UserId); // ✅ คุณ assert มันอย่างตั้งใจตรง boundary

brand มีอยู่แค่ตอน compile time (ถูก erase ทิ้ง), ไม่มี cost ตอน runtime และบังคับให้ทุก UserId ถูก สร้างอย่างตั้งใจ — โดยปกติคือทันทีหลัง validate นี่คือวิธีได้ nominal safety ในระบบที่เป็น structural

ทำไมควรเลือก `unknown` มากกว่า `any` สำหรับ value จาก `JSON.parse`?
ข้อดีของ schema library อย่าง zod ตรง boundary คืออะไร?
branded type แก้ปัญหาอะไร?
type guard function มี return type เป็น `v is User` ทำอะไร?