บทนำ
Type ใน TypeScript ไม่ใช่แค่การแปะป้ายชื่อว่านี่คือ string หรือ number แต่มันคือ "Set theory" (ทฤษฎีเซต) เราสามารถเอา Type มารวมกัน (Union) หรือหาจุดตัด (Intersection) เพื่อสร้างข้อกำหนดข้อมูลที่ซับซ้อนและแม่นยำได้
1. Union Types (|) คือ "OR" logic
Union Type บอกว่าค่านี้เป็นได้ "อย่างใดอย่างหนึ่ง"
type ID = string | number; // เป็น string ก็ได้ หรือ number ก็ได้
ความเข้าใจเชิงลึก: เมื่อตัวแปรเป็น Union Type เราจะเรียกใช้ method ได้เฉพาะ ส่วนที่มีร่วมกัน เท่านั้น จนกว่าจะมีการตรวจสอบ Type (Narrowing)
function printId(id: string | number) {
// ❌ Error: toUpperCase มีแค่ใน string ไม่มีใน number
// console.log(id.toUpperCase());
// ✅ ต้อง Narrowing ก่อน
if (typeof id === "string") {
console.log(id.toUpperCase());
}
}
2. Intersection Types (&) คือ "AND" logic
ใช้รวมคุณสมบัติของ Type เข้าด้วยกัน (เหมือนการ Merge Object)
interface Draggable {
drag: () => void;
}
interface Resizable {
resize: () => void;
}
// UIComponent ต้องทำได้ *ทั้ง* drag และ resize
type UIComponent = Draggable & Resizable;
const box: UIComponent = {
drag: () => {},
resize: () => {},
};
3. Discriminated Unions (Tagged Unions) 🌟
นี่คือเทคนิคขั้นสูงที่ทรงพลังที่สุดในการจัดการ State
สมมติเรามี API State ที่มี 3 สถานะ: Loading, Success, Error
แทนที่จะใช้ตัวแปรแยกกัน (isLoading, error, data) ที่อาจขัดแย้งกันได้ ให้ใช้ Discriminated Union โดยมี "ตัวแบ่งแยก" (Discriminator) เช่น field kind หรือ status
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: string[] };
type ErrorState = { status: "error"; error: Error };
// State เป็นได้แค่ 3 แบบนี้เท่านั้น! เป็นแบบอื่นไม่ได้
type ApiState = LoadingState | SuccessState | ErrorState;
การใช้งาน: TS จะฉลาดมาก (Control Flow Analysis) รู้ทันทีว่าในแต่ละเคสมีตัวแปรอะไรให้ใช้บ้าง
function render(state: ApiState) {
switch (state.status) {
case "loading":
// ตรงนี้ state คือ LoadingState
return "Loading...";
case "success":
// ตรงนี้ state คือ SuccessState (เรียก .data ได้)
return `Data: ${state.data.join(", ")}`;
case "error":
// ตรงนี้ state คือ ErrorState (เรียก .error ได้)
return `Error: ${state.error.message}`;
}
}
ทำไมวิธีนี้ถึงดีที่สุด? มันป้องกัน Invalid State เช่น isLoading: true แต่มี error โผล่มาด้วย ซึ่งเป็นบั๊กที่เจอบ่อยมากในการจัดการ State แบบแยกตัวแปร
4. any vs unknown vs never
any: ปิดตาเดิน
เมื่อใช้ any คือการบอก Compiler ว่า "เชื่อฉันเถอะ อย่ามายุ่ง" ผลคือเราเสียความปลอดภัยทั้งหมด
Best Practice: ห้ามใช้ ถ้าไม่จำเป็นจริงๆ
unknown: ปิดตาแต่ต้องคลำทาง
unknown เหมือน any คือรับค่าอะไรก็ได้ แต่ ห้ามนำไปใช้งานจนกว่าจะตรวจสอบ Type
let value: unknown;
value = "Hello";
// ❌ Error: Object is of type 'unknown'
// console.log(value.toUpperCase());
// ✅ ต้องเช็คก่อน
if (typeof value === "string") {
console.log(value.toUpperCase());
}
never: ทางตัน
ใช้สำหรับค่าที่ "ไม่มีวันเกิดขึ้นจริง" มักใช้ในการทำ Exhaustiveness Checking ใน Switch case
function unknownState(state: never): never {
throw new Error("Unexpected state");
}
// ใน Switch case ของหัวข้อที่ 3 ถ้าเราลืม case "error"
// TS จะฟ้องว่าตัวแปร state ไม่ใช่ never (เพราะมันเหลือเคส error ที่ยังไม่ได้ดัก)
สรุป
ระบบ Type ของ TypeScript ถูกออกแบบมาให้เรา "จำลองโมเดลของข้อมูล" ได้อย่างแม่นยำ การใช้ Discriminated Unions ร่วมกับ Type Narrowing คือกุญแจสำคัญที่จะทำให้โค้ดของคุณ Solid แข็งแกร่ง ยากที่จะเกิด Runtime Error