Nullability & never
strictNullChecks: setting ที่สำคัญที่สุด
หัวข้อที่มีชื่อว่า “strictNullChecks: setting ที่สำคัญที่สุด”flag ที่มีค่ามากที่สุดตัวเดียวคือ strictNullChecks (เป็นส่วนหนึ่งของ strict) ถ้าไม่เปิด null และ undefined assign เข้า ทุก type ได้ — billion-dollar mistake แบบเต็มพิกัด พอเปิด จะกลายเป็น type ของตัวเองที่คุณต้องจัดการอย่างชัดเจน
// เมื่อเปิด strictNullChecks:function len(s: string) { return s.length; }
let maybe: string | undefined;len(maybe); // ❌ Argument of type 'string | undefined' is not assignable to 'string'error นี่แหละคือประเด็น: compiler บังคับให้คุณจัดการเคส undefined ก่อน จะ dereference เปิด flag นี้ไว้ ถ้าคุณรับ codebase ที่ยังไม่ได้เปิดมา การเปิดคือ migration ที่ให้ผลตอบแทนสูงสุดที่คุณทำได้
?. และ ?? เข้าใจ type
หัวข้อที่มีชื่อว่า “?. และ ?? เข้าใจ type”operator สองตัวทำให้ nullability ใช้ง่ายขึ้น และ type system เข้าใจทั้งคู่:
type User = { profile?: { avatar?: string } };
function avatar(u: User) { const url = u.profile?.avatar; // type: string | undefined return url ?? "default.png"; // type: string (?? ให้ fallback มา)}?. short-circuit เป็น undefined ถ้าฝั่งซ้าย nullish และ type ก็สะท้อนสิ่งนั้น (ผลลัพธ์รวม undefined) ?? ให้ fallback เฉพาะกรณี null/undefined (ต่างจาก || ที่ trigger กับ 0 และ "" ด้วย) และ narrow type โดยตัดส่วน nullish ออก
non-null assertion ! — ปืนที่บรรจุกระสุน
หัวข้อที่มีชื่อว่า “non-null assertion ! — ปืนที่บรรจุกระสุน”! แบบ postfix บอก compiler ว่า “เชื่อฉัน อันนี้ไม่ null”:
const el = document.getElementById("app")!; // assert ว่าไม่ nullel.innerHTML = "hi"; // ไม่ error — แต่ crash ถ้า #app ไม่มีอยู่! ลบ check จริงทิ้งโดยไม่มีการ verify ตอน runtime เลย บางครั้งคุณรู้มากกว่า compiler จริง ๆ — แต่ทุก ! คือจุดที่คุณ override safety net ไว้ และเป็นจุดที่การเปลี่ยนแปลงในอนาคตทำให้พังเงียบ ๆ ได้ เลือก check จริง (if (!el) throw ...) ที่ narrow อย่างซื่อสัตย์ดีกว่า เก็บ ! ไว้สำหรับเคสที่พิสูจน์ได้
การ model error: Result vs throw
หัวข้อที่มีชื่อว่า “การ model error: Result vs throw”exception ที่ throw มองไม่เห็นจาก type system — signature ของ function ไม่เคยบอกว่าอาจ throw อะไร Result type ทำให้ failure ชัดเจนและ type-checked:
type Result<T, E> = | { ok: true; value: T } | { ok: false; error: E };
function parsePort(s: string): Result<number, string> { const n = Number(s); if (!Number.isInteger(n) || n < 0) return { ok: false, error: "invalid port" }; return { ok: true, value: n };}
const r = parsePort(input);if (r.ok) r.value; // narrow เป็น numberelse r.error; // narrow เป็น stringตอนนี้ caller ต้อง จัดการทั้งสอง branch — compiler ไม่ยอมให้ลืม นี่คือ discriminated union (field ok เป็น tag) ซึ่งพาเราไปสู่เครื่องมือสุดท้าย
Exhaustiveness ด้วย never
หัวข้อที่มีชื่อว่า “Exhaustiveness ด้วย never”เมื่อคุณ switch บน discriminated union never การันตีว่าคุณจัดการครบทุกเคส assign value เป็น never ใน branch default: ถ้ามีการเพิ่ม variant ใหม่ การ assign นั้นจะ compile ไม่ผ่าน
type Shape = | { kind: "circle"; r: number } | { kind: "square"; side: number };
function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.r ** 2; case "square": return s.side ** 2; default: const _exhaustive: never = s; // ✅ s เป็น never ตรงนี้ — ครบทุกเคสแล้ว return _exhaustive; }}เพิ่ม variant triangle ทีหลังแล้วบรรทัด default จะ error: Type '{ kind: "triangle" ... }' is not assignable to type 'never' compiler เพิ่งเปลี่ยน “ฉัน update ทุก switch แล้วหรือยัง?” จากการไล่หาด้วยมือให้เป็น build error
flowchart TB u["Shape = circle or square"] --> c1["case circle: handled"] u --> c2["case square: handled"] c1 --> d["default: assign s to never"] c2 --> d d --> ok["compiles = all cases covered"] d --> err["new variant unhandled = compile error"]