Type Inference
คุณเขียน type น้อยกว่าที่คิด
หัวข้อที่มีชื่อว่า “คุณเขียน type น้อยกว่าที่คิด”TypeScript ที่ดีไม่ได้เต็มไปด้วย annotation compiler infer type ส่วนใหญ่ให้ และการฝืน inference ด้วย annotation ที่ซ้ำซ้อนคือสัญญาณของมือใหม่ ทักษะที่แท้จริงคือรู้ว่า infer อะไร และ ตรงไหน ที่คุณต้องเข้าไปช่วยจริง ๆ
let count = 5; // inferred: numberconst name = "Ada"; // inferred: "Ada" (a literal type!)const items = [1, 2]; // inferred: number[]สังเกตว่า count กับ name ถูก infer ต่างกัน และความต่างนั้นคือหนึ่งในกฎที่สำคัญที่สุดของภาษา
let widen, const เก็บ literal
หัวข้อที่มีชื่อว่า “let widen, const เก็บ literal”const reassign ไม่ได้เลย compiler จึงเก็บ type ที่ แคบสุด ไว้ — คือ literal ส่วน let reassign ได้ compiler จึง widen ไปเป็น type ทั่วไป
const a = "hello"; // type: "hello" (literal — it can't change)let b = "hello"; // type: string (widened — b might become any string)นี่คือเหตุผลที่พฤติกรรม “โผล่มาแล้วก็หายไป” ของ literal type สำคัญอยู่ตลอด:
const method = "GET"; // type: "GET"let method2 = "GET"; // type: string
function req(m: "GET" | "POST") {}req(method); // ✅ "GET" fits the unionreq(method2); // ❌ string is too wide for "GET" | "POST"flowchart LR lit["literal value GET"] -->|const: เปลี่ยนไม่ได้| keep["type: GET (แคบ)"] lit -->|let: อาจเปลี่ยนได้| wide["type: string (ถูก widen)"]
เมื่อคุณอยากได้ literal type จาก let หรือจาก property ของ object ให้ใช้ const assertion (as const) — เป็นหัวข้อเต็ม ๆ ในโมดูลถัดไป
Contextual typing: inference ไหลเข้าด้านใน
หัวข้อที่มีชื่อว่า “Contextual typing: inference ไหลเข้าด้านใน”inference ไม่ได้ไหลจากล่างขึ้นบน (จาก value ไปหา type) อย่างเดียว แต่ไหลจาก บนลงล่าง จาก context รอบ ๆ ด้วย — นี่คือ contextual typing
const nums = [1, 2, 3];
nums.forEach((n) => { // n is inferred as number — you didn't annotate it. // The context (an array of number) told the compiler what n must be. console.log(n.toFixed(2));});
window.addEventListener("click", (e) => { // e is inferred as MouseEvent from the event name — pure contextual typing.});นี่คือเหตุผลที่ parameter ของ callback มักไม่ต้อง annotate: function ที่รับ callback ตัวนั้นประกาศ type ของ parameter ไว้แล้ว inference จึงไหลเข้ามา
Best common type
หัวข้อที่มีชื่อว่า “Best common type”เมื่อ value อาจเป็นได้หลาย type — เช่น array ที่มี element ปนกัน — compiler จะคำนวณ best common type ที่ครอบคลุมทั้งหมด:
const mixed = [1, "two", 3]; // inferred: (string | number)[]const shapes = [circle, square]; // inferred: (Circle | Square)[]ไม่ได้เลือกอันหนึ่งแล้ว error ที่เหลือ แต่สร้าง union ที่เข้าได้กับทุก element บางครั้ง best common type กว้างกว่าที่คุณอยากได้ ที่เป็นสัญญาณว่าควร annotate ให้ชัด
เมื่อไรควร annotate
หัวข้อที่มีชื่อว่า “เมื่อไรควร annotate”ปล่อยให้ inference ทำงาน แล้ว annotate อย่างจงใจใน 3 จุด:
- parameter ของ function — inference อ่านใจคุณเรื่อง input ไม่ได้ (แม้ contextual typing จะครอบคลุม callback ให้)
- return type ของ function บน public API — การ annotate return type จับความผิดพลาด ภายใน function และบอก intent (inference ทำงานได้ แต่ return type ที่ระบุชัดคือ guardrail)
- เมื่อ inference กว้างกว่าที่คุณอยากได้ — เช่นคุณต้องการ literal หรือ union เฉพาะ ไม่ใช่ type ที่ถูก widen
ที่เหลือทั้งหมดให้ใช้ inference annotation ที่ซ้ำซ้อนคือ noise ที่อาจ เพี้ยน ไปจากความจริงตอน refactor