Everyday Types, Deep
type ที่คุณใช้อยู่แล้ว — แบบเข้าใจจริง
หัวข้อที่มีชื่อว่า “type ที่คุณใช้อยู่แล้ว — แบบเข้าใจจริง”คุณหยิบพวกนี้มาใช้ทุกวัน: string literal, union, object shape, tuple, function โมดูลนี้จะค่อย ๆ เจาะจุดที่คนมักสะดุด — widening, as const, ความต่างระหว่าง union กับ intersection และ control-flow narrowing ที่ทำให้ union ใช้งานได้จริง
ทุกอย่างในนี้ตั้งอยู่บน model จาก Foundations: type คือเซตของ value ถือ lens นี้ไว้ แล้วแต่ละ feature ก็แค่วิธีอธิบายหรือย่อเซตให้เล็กลง
โมดูลนี้ครอบคลุมอะไรบ้าง
หัวข้อที่มีชื่อว่า “โมดูลนี้ครอบคลุมอะไรบ้าง”| บทเรียน | สิ่งที่คุณจะได้เรียนรู้ |
|---|---|
| Literals & widening | literal type, จะ widen เมื่อไร และ as const |
| Unions & intersections | A | B (กว้างกว่า) เทียบ A & B (แคบกว่า) และวิธีทำงานกับแต่ละแบบ |
| Objects, arrays & tuples | property แบบ optional/readonly, index signature, tuple |
| Functions | overload, this typing, rest param, ประเด็น void |
| Narrowing | เปลี่ยน union ให้เป็น type เฉพาะด้วย control flow |
เส้นเรื่องหลัก: describe แล้วค่อย narrow
หัวข้อที่มีชื่อว่า “เส้นเรื่องหลัก: describe แล้วค่อย narrow”flowchart LR describe["Describe the shape (literals, unions, objects)"] --> hold["Hold a wide type (a union of possibilities)"] hold --> narrow["Narrow with control flow (typeof, in, discriminants)"] narrow --> use["Use the specific type safely"]
คุณ describe ข้อมูลด้วย union และ object type ซึ่งทำให้คุณถือ type ที่ กว้าง อยู่ (มีหลายความเป็นไปได้) จากนั้นก็ narrow — ด้วย typeof, in, discriminant field — ลงมาเหลือ case เฉพาะตัวเดียว แล้ว TypeScript ก็ยอมให้ใช้งานอย่างปลอดภัย เข้าใจ loop นี้แล้วคุณก็เข้าใจ TypeScript ในชีวิตประจำวัน
รวมทุกอย่างเข้าด้วยกัน
หัวข้อที่มีชื่อว่า “รวมทุกอย่างเข้าด้วยกัน”ทุกบทในโมดูลนี้โผล่มาในรูปเดียวกัน: literal union สำหรับ value ที่อนุญาต, object type สำหรับ request, discriminated union สำหรับ response และ switch ที่ narrow มัน
// @runtype Method = "GET" | "POST"; // literal union -- describe
interface ApiRequest { readonly method: Method; path: string;}
type ApiResponse = | { kind: "ok"; status: 200; data: unknown } | { kind: "error"; status: 400 | 404 | 500; message: string };
function describe(res: ApiResponse): string { switch (res.kind) { case "ok": return `OK: ${JSON.stringify(res.data)}`; case "error": return `Error ${res.status}: ${res.message}`; default: { const _exhaustive: never = res; // exhaustiveness -- a new kind fails to compile here return _exhaustive; } }}
const req: ApiRequest = { method: "GET", path: "/users" };console.log(req.method, req.path);console.log(describe({ kind: "ok", status: 200, data: { id: 1 } }));console.log(describe({ kind: "error", status: 404, message: "not found" }));// GET /users// OK: {"id":1}// Error 404: not foundDescribe, then narrow
type Method = "GET" | "POST"; // literal union -- describe
interface ApiRequest { readonly method: Method; path: string;}
type ApiResponse = | { kind: "ok"; status: 200; data: unknown } | { kind: "error"; status: 400 | 404 | 500; message: string };
function describe(res: ApiResponse): string { switch (res.kind) { case "ok": return `OK: ${JSON.stringify(res.data)}`; case "error": return `Error ${res.status}: ${res.message}`; default: { const _exhaustive: never = res; // exhaustiveness -- a new kind fails to compile here return _exhaustive; } }}
const req: ApiRequest = { method: "GET", path: "/users" };console.log(req.method, req.path);
console.log(describe({ kind: "ok", status: 200, data: { id: 1 } }));console.log(describe({ kind: "error", status: 404, message: "not found" })); Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart LR lit["Literals & widening pin down exact values"] --> ui["Unions & intersections combine (OR) or compose (AND)"] ui --> oat["Objects, arrays & tuples shape the data"] oat --> fn["Functions shape the behavior"] fn --> nar["Narrowing collapse the union at use-time"]
มองห้าบทนี้เป็น pipeline เดียว ไม่ใช่ห้าเรื่องแยกกัน: คุณตรึง value ที่ชัดเจนด้วย literal รวม (OR) หรือประกอบ (AND) มันด้วย union กับ intersection สร้างรูปร่างข้อมูลด้วย object, array และ tuple สร้างรูปร่างพฤติกรรมด้วย function และสุดท้ายยุบทุกอย่างกลับลงมาด้วย narrowing ตรงจุดที่คุณใช้ value จริง ๆ กระโดดไปใช้ any หรือ string แบบหลวม ๆ ในขั้นไหนก็ตัดวงจรนี้ทันที — ผลตอบแทนของทั้งโมดูลจะเห็นก็ต่อเมื่อทุกขั้นแม่นยำ และ switch/if สุดท้าย narrow ลงมาเหลือของจริง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| แนวทาง | ได้อะไร | เสียอะไร |
|---|---|---|
type หลวม ๆ (string, object, any) | เขียนไว เข้าใจไม่ต้อง model ล่วงหน้า | ไม่มี autocomplete ไม่มี exhaustiveness bug โผล่ตอน runtime แทนที่จะเป็น compile time |
| literal union + discriminated union | compiler บังคับให้ handle ทุก case (never exhaustiveness) | ต้องประกาศ type ล่วงหน้ามากขึ้น เพิ่ม case ใหม่คือการแก้ที่ตั้งใจและมองเห็นได้ทุกจุดที่ใช้ |
object shape ด้วย interface/type | อธิบายตัวเอง ตรวจทุก call site | มี ceremony เกินจำเป็นสำหรับ object เล็ก ๆ ที่ใช้ครั้งเดียว |
| narrowing ด้วย control flow | ไม่มีต้นทุนตอน runtime อ่านเหมือน JS ปกติ | ดีเท่าที่เช็คที่คุณเขียนเท่านั้น — ข้อจำกัดของ narrowing (บทหลัง) อาจทำให้มั่นใจผิด ๆ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- describe type แล้วคือปลอดภัยแล้ว type ปกป้องคุณแค่ตรงจุดที่ compiler ยังมองเห็นมันอยู่ —
switch/ifที่ narrow union ตรงจุดที่คุณใช้ต่างหากที่จับ bug ได้จริง - type กว้างกว่าปลอดภัยกว่าเสมอ ใช้
stringแทน literal union ทำให้ compiler จับ typo อย่าง"colour"ไม่ได้เลย — แค่ย้าย bug ไปโผล่ที่ production interfaceกับtypeต่างกันอย่างมีนัยสำคัญสำหรับ object ธรรมดา สำหรับ object shape ทั้งสองใกล้เคียงกันมาก ต่างกันจริงแค่เรื่อง declaration merging, union หรือ mapped type
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ก่อนเชื่อ function ที่ generate มาแทน union type ให้เช็คสิ่งเหล่านี้:
switchบน discriminated union ที่ไม่มีdefault: neverguard — วันที่มี case ใหม่เพิ่มเข้ามาจะเงียบ ๆ ไม่ exhaustive อีกต่อไป- function ที่
switch/ifchain ไม่ครอบคลุมทุก branch ด้วยreturn— inferred return type จะแอบเพิ่ม| undefinedที่โผล่เป็น error ทีหลังตอนใช้งานจริง - parameter ที่ type เป็น
stringหรือobjectทั้งที่มี literal union หรือ interface เฉพาะเจาะจงให้ใช้ — สัญญาณว่าคน (หรือ AI) ไม่ได้ model value ที่อนุญาตจริง ๆ
🐛 หา bug ในโค้ดนี้
type Shape =| { kind: "circle"; radius: number }| { kind: "square"; side: number };
function area(s: Shape) {if (s.kind === "circle") return Math.PI * s.radius ** 2;if (s.kind === "square") return s.side ** 2;}
const s: Shape = { kind: "circle", radius: 2 };console.log(area(s).toFixed(2));เฉลย
ทั้งสอง if ไม่ได้เขียนเป็น switch แบบ exhaustive TypeScript เลยพิสูจน์ไม่ได้ว่าทุก Shape return เป็นตัวเลข — inferred return type ของ area คือ number | undefined diagnostic จริงบน area(s).toFixed(2) คือ TS2532: Object is possibly 'undefined' ทางแก้คือ pattern discriminated union จากบท narrowing: switch บน kind ที่มี default branch assign เข้า never เพื่อให้ case ที่หายไปเป็น compile error แทนที่จะเป็น undefined แบบเงียบ ๆ