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

Type Inference

TypeScript ที่ดีไม่ได้เต็มไปด้วย annotation compiler infer type ส่วนใหญ่ให้ และการฝืน inference ด้วย annotation ที่ซ้ำซ้อนคือสัญญาณของมือใหม่ ทักษะที่แท้จริงคือรู้ว่า infer อะไร และ ตรงไหน ที่คุณต้องเข้าไปช่วยจริง ๆ

let count = 5; // inferred: number
const name = "Ada"; // inferred: "Ada" (a literal type!)
const items = [1, 2]; // inferred: number[]

สังเกตว่า count กับ name ถูก infer ต่างกัน และความต่างนั้นคือหนึ่งในกฎที่สำคัญที่สุดของภาษา

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 union
req(method2); // ❌ string is too wide for "GET" | "POST"
flowchart LR
  lit["literal value
GET"] -->|const: เปลี่ยนไม่ได้| keep["type: GET
(แคบ)"]
  lit -->|let: อาจเปลี่ยนได้| wide["type: string
(ถูก widen)"]
const เก็บ literal; let widen

เมื่อคุณอยากได้ literal type จาก let หรือจาก property ของ object ให้ใช้ const assertion (as const) — เป็นหัวข้อเต็ม ๆ ในโมดูลถัดไป

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 จึงไหลเข้ามา

เมื่อ 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 ให้ชัด

ปล่อยให้ inference ทำงาน แล้ว annotate อย่างจงใจใน 3 จุด:

  1. parameter ของ function — inference อ่านใจคุณเรื่อง input ไม่ได้ (แม้ contextual typing จะครอบคลุม callback ให้)
  2. return type ของ function บน public API — การ annotate return type จับความผิดพลาด ภายใน function และบอก intent (inference ทำงานได้ แต่ return type ที่ระบุชัดคือ guardrail)
  3. เมื่อ inference กว้างกว่าที่คุณอยากได้ — เช่นคุณต้องการ literal หรือ union เฉพาะ ไม่ใช่ type ที่ถูก widen

ที่เหลือทั้งหมดให้ใช้ inference annotation ที่ซ้ำซ้อนคือ noise ที่อาจ เพี้ยน ไปจากความจริงตอน refactor

`const x = "GET"` ถูก infer เป็น type อะไร?
ทำไม `let y = "GET"` ถึง infer เป็น `string` ขณะที่ `const x = "GET"` infer เป็น `"GET"`?
ทำไม parameter ของ callback มักไม่ต้อง annotate type?
TypeScript infer อะไรให้ `[1, "two", 3]`?