ไทย

สำรวจ TypeScript discriminated unions เครื่องมืออันทรงพลังสำหรับสร้าง state machine ที่แข็งแกร่งและปลอดภัยต่อประเภทข้อมูล เรียนรู้วิธีการกำหนดสถานะ จัดการการเปลี่ยนสถานะ และใช้ประโยชน์จากระบบประเภทข้อมูลของ TypeScript เพื่อเพิ่มความน่าเชื่อถือของโค้ด

TypeScript Discriminated Unions: การสร้าง State Machines ที่ปลอดภัยต่อประเภทข้อมูล (Type-Safe)

ในโลกของการพัฒนาซอฟต์แวร์ การจัดการสถานะ (state) ของแอปพลิเคชันอย่างมีประสิทธิภาพเป็นสิ่งสำคัญอย่างยิ่ง State machines เป็นเครื่องมืออันทรงพลังที่ใช้ในการจำลองระบบที่มีสถานะซับซ้อน ช่วยให้มั่นใจได้ว่าพฤติกรรมของระบบจะเป็นไปตามที่คาดการณ์ และทำให้การทำความเข้าใจตรรกะของระบบง่ายขึ้น TypeScript ซึ่งมีระบบประเภทข้อมูล (type system) ที่แข็งแกร่ง นำเสนอกลไกที่ยอดเยี่ยมสำหรับการสร้าง state machines ที่ปลอดภัยต่อประเภทข้อมูลโดยใช้ discriminated unions (หรือที่เรียกว่า tagged unions หรือ algebraic data types)

Discriminated Unions คืออะไร?

Discriminated union คือประเภทข้อมูล (type) ที่แทนค่าซึ่งสามารถเป็นหนึ่งในหลายประเภทที่แตกต่างกันได้ โดยแต่ละประเภทเหล่านี้ ซึ่งเรียกว่าสมาชิกของ union จะมีคุณสมบัติร่วมกันที่ชัดเจนและแตกต่างกัน เรียกว่า discriminant หรือ tag ซึ่ง discriminant นี้เองที่ช่วยให้ TypeScript สามารถระบุได้อย่างแม่นยำว่าสมาชิกตัวใดของ union กำลังทำงานอยู่ ทำให้สามารถตรวจสอบประเภทข้อมูลและเติมโค้ดอัตโนมัติได้อย่างมีประสิทธิภาพ

ลองนึกภาพเหมือนสัญญาณไฟจราจร ที่สามารถอยู่ในสถานะใดสถานะหนึ่งจากสามสถานะ: แดง, เหลือง, หรือเขียว คุณสมบัติ 'color' ทำหน้าที่เป็น discriminant ที่บอกเราได้อย่างชัดเจนว่าสัญญาณไฟอยู่ในสถานะใด

เหตุใดจึงควรใช้ Discriminated Unions สำหรับ State Machines?

Discriminated unions มีประโยชน์หลักหลายประการเมื่อสร้าง state machines ใน TypeScript:

การกำหนด State Machine ด้วย Discriminated Unions

เรามาดูตัวอย่างการกำหนด state machine โดยใช้ discriminated unions ด้วยตัวอย่างที่เป็นรูปธรรม: ระบบประมวลผลคำสั่งซื้อ คำสั่งซื้อสามารถอยู่ในสถานะต่อไปนี้: Pending (รอการดำเนินการ), Processing (กำลังดำเนินการ), Shipped (จัดส่งแล้ว), และ Delivered (ส่งมอบแล้ว)

ขั้นตอนที่ 1: กำหนดประเภทของสถานะ (State Types)

ขั้นแรก เราจะกำหนด type แยกสำหรับแต่ละสถานะ แต่ละ type จะมี property `type` ทำหน้าที่เป็น discriminant พร้อมกับข้อมูลเฉพาะของสถานะอื่นๆ


interface Pending {
  type: "pending";
  orderId: string;
  customerName: string;
  items: string[];
}

interface Processing {
  type: "processing";
  orderId: string;
  assignedAgent: string;
}

interface Shipped {
  type: "shipped";
  orderId: string;
  trackingNumber: string;
}

interface Delivered {
  type: "delivered";
  orderId: string;
  deliveryDate: Date;
}

ขั้นตอนที่ 2: สร้าง Discriminated Union Type

ถัดไป เราจะสร้าง discriminated union โดยการรวม type แต่ละตัวเข้าด้วยกันโดยใช้ตัวดำเนินการ `|` (union)


type OrderState = Pending | Processing | Shipped | Delivered;

ตอนนี้ `OrderState` แทนค่าที่สามารถเป็น `Pending`, `Processing`, `Shipped`, หรือ `Delivered` ได้ โดย property `type` ภายในแต่ละสถานะทำหน้าที่เป็น discriminant ซึ่งช่วยให้ TypeScript สามารถแยกแยะระหว่างสถานะเหล่านี้ได้

การจัดการการเปลี่ยนสถานะ (State Transitions)

เมื่อเราได้กำหนด state machine ของเราแล้ว เราต้องการกลไกในการเปลี่ยนสถานะ ลองสร้างฟังก์ชัน `processOrder` ที่รับสถานะปัจจุบันและ action เป็นอินพุต และส่งคืนสถานะใหม่


interface Action {
  type: string;
  payload?: any;
}

function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    case "pending":
      if (action.type === "startProcessing") {
        return {
          type: "processing",
          orderId: state.orderId,
          assignedAgent: action.payload.agentId,
        };
      }
      return state; // ไม่มีการเปลี่ยนแปลงสถานะ

    case "processing":
      if (action.type === "shipOrder") {
        return {
          type: "shipped",
          orderId: state.orderId,
          trackingNumber: action.payload.trackingNumber,
        };
      }
      return state; // ไม่มีการเปลี่ยนแปลงสถานะ

    case "shipped":
      if (action.type === "deliverOrder") {
        return {
          type: "delivered",
          orderId: state.orderId,
          deliveryDate: new Date(),
        };
      }
      return state; // ไม่มีการเปลี่ยนแปลงสถานะ

    case "delivered":
      // คำสั่งซื้อถูกส่งมอบแล้ว ไม่มีการดำเนินการเพิ่มเติม
      return state;

    default:
      // กรณีนี้ไม่ควรเกิดขึ้นเนื่องจากการตรวจสอบความครบถ้วน
      return state; // หรือ throw an error
  }
}

คำอธิบาย

การใช้ประโยชน์จากการตรวจสอบความครบถ้วน (Exhaustiveness Checking)

การตรวจสอบความครบถ้วนของ TypeScript เป็นคุณสมบัติที่ทรงพลังที่ช่วยให้แน่ใจว่าคุณได้จัดการกับทุกสถานะที่เป็นไปได้ใน state machine ของคุณ หากคุณเพิ่มสถานะใหม่เข้าไปใน `OrderState` union แต่ลืมอัปเดตฟังก์ชัน `processOrder` TypeScript จะแจ้งข้อผิดพลาด

เพื่อเปิดใช้งานการตรวจสอบความครบถ้วน คุณสามารถใช้ `never` type ได้ โดยภายใน `default` case ของคำสั่ง switch ของคุณ ให้กำหนดค่า state ให้กับตัวแปรประเภท `never`


function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    // ... (cases ก่อนหน้า) ...

    default:
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck; // หรือ throw an error
  }
}

หากคำสั่ง `switch` จัดการกับค่า `OrderState` ที่เป็นไปได้ทั้งหมดแล้ว ตัวแปร `_exhaustiveCheck` จะเป็นประเภท `never` และโค้ดจะคอมไพล์ผ่าน อย่างไรก็ตาม หากคุณเพิ่มสถานะใหม่เข้าไปใน `OrderState` union และลืมจัดการในคำสั่ง `switch` ตัวแปร `_exhaustiveCheck` จะกลายเป็นประเภทอื่น และ TypeScript จะแสดงข้อผิดพลาดขณะคอมไพล์ เพื่อแจ้งเตือนคุณถึง case ที่ขาดหายไป

ตัวอย่างและการใช้งานจริง

Discriminated unions สามารถนำไปประยุกต์ใช้ได้ในหลากหลายสถานการณ์นอกเหนือจากระบบประมวลผลคำสั่งซื้อธรรมดา:

ตัวอย่าง: การจัดการสถานะของ UI

ลองพิจารณาตัวอย่างง่ายๆ ของการจัดการสถานะของ UI component ที่ดึงข้อมูลจาก API เราสามารถกำหนดสถานะต่อไปนี้ได้:


interface Initial {
  type: "initial";
}

interface Loading {
  type: "loading";
}

interface Success {
  type: "success";
  data: T;
}

interface Error {
  type: "error";
  message: string;
}

type UIState = Initial | Loading | Success | Error;

function renderUI(state: UIState): React.ReactNode {
  switch (state.type) {
    case "initial":
      return 

คลิกปุ่มเพื่อโหลดข้อมูล

; case "loading": return

กำลังโหลด...

; case "success": return
{JSON.stringify(state.data, null, 2)}
; case "error": return

ข้อผิดพลาด: {state.message}

; default: const _exhaustiveCheck: never = state; return _exhaustiveCheck; } }

ตัวอย่างนี้แสดงให้เห็นว่า discriminated unions สามารถใช้เพื่อจัดการสถานะต่างๆ ของ UI component ได้อย่างมีประสิทธิภาพ ทำให้มั่นใจได้ว่า UI จะแสดงผลอย่างถูกต้องตามสถานะปัจจุบัน ฟังก์ชัน `renderUI` จะจัดการแต่ละสถานะอย่างเหมาะสม ซึ่งเป็นวิธีที่ชัดเจนและปลอดภัยต่อประเภทข้อมูลในการจัดการ UI

แนวปฏิบัติที่ดีที่สุดสำหรับการใช้ Discriminated Unions

เพื่อใช้ discriminated unions ในโปรเจกต์ TypeScript ของคุณอย่างมีประสิทธิภาพ ลองพิจารณาแนวปฏิบัติที่ดีที่สุดต่อไปนี้:

เทคนิคขั้นสูง

Conditional Types

Conditional types สามารถใช้ร่วมกับ discriminated unions เพื่อสร้าง state machines ที่ทรงพลังและยืดหยุ่นมากยิ่งขึ้น ตัวอย่างเช่น คุณสามารถใช้ conditional types เพื่อกำหนดประเภทการคืนค่าที่แตกต่างกันสำหรับฟังก์ชันโดยขึ้นอยู่กับสถานะปัจจุบัน


function getData(state: UIState): T | undefined {
  if (state.type === "success") {
    return state.data;
  }
  return undefined;
}

ฟังก์ชันนี้ใช้คำสั่ง `if` ง่ายๆ แต่สามารถทำให้แข็งแกร่งขึ้นได้โดยใช้ conditional types เพื่อให้แน่ใจว่าจะมีการคืนค่าประเภทที่เฉพาะเจาะจงเสมอ

Utility Types

Utility types ของ TypeScript เช่น `Extract` และ `Omit` สามารถเป็นประโยชน์เมื่อทำงานกับ discriminated unions `Extract` ช่วยให้คุณสามารถดึงสมาชิกเฉพาะออกจาก union type ตามเงื่อนไข ในขณะที่ `Omit` ช่วยให้คุณสามารถลบ properties ออกจาก type ได้


// ดึงสถานะ "success" ออกจาก UIState union
type SuccessState = Extract, { type: "success" }>;

// ลบ property 'message' ออกจาก Error interface
type ErrorWithoutMessage = Omit;

ตัวอย่างในโลกแห่งความเป็นจริงในอุตสาหกรรมต่างๆ

พลังของ discriminated unions แผ่ขยายไปทั่วอุตสาหกรรมและโดเมนแอปพลิเคชันต่างๆ:

สรุป

TypeScript discriminated unions เป็นวิธีที่ทรงพลังและปลอดภัยต่อประเภทข้อมูลในการสร้าง state machines ด้วยการกำหนดสถานะและการเปลี่ยนสถานะที่เป็นไปได้อย่างชัดเจน คุณสามารถสร้างโค้ดที่แข็งแกร่ง บำรุงรักษาง่าย และเข้าใจง่ายขึ้น การผสมผสานระหว่าง type safety, exhaustiveness checking และการเติมโค้ดอัตโนมัติที่ดีขึ้น ทำให้ discriminated unions เป็นเครื่องมือที่ทรงคุณค่าสำหรับนักพัฒนา TypeScript ทุกคนที่ต้องจัดการกับสถานะที่ซับซ้อน ลองนำ discriminated unions ไปใช้ในโปรเจกต์ถัดไปของคุณและสัมผัสกับประโยชน์ของการจัดการสถานะที่ปลอดภัยต่อประเภทข้อมูลด้วยตัวคุณเอง ดังที่เราได้แสดงให้เห็นด้วยตัวอย่างที่หลากหลายตั้งแต่อีคอมเมิร์ซไปจนถึงการดูแลสุขภาพ และโลจิสติกส์ไปจนถึงการศึกษา หลักการของการจัดการสถานะที่ปลอดภัยต่อประเภทข้อมูลผ่าน discriminated unions สามารถนำไปประยุกต์ใช้ได้ในระดับสากล

ไม่ว่าคุณจะสร้าง UI component ง่ายๆ หรือแอปพลิเคชันระดับองค์กรที่ซับซ้อน discriminated unions สามารถช่วยให้คุณจัดการสถานะได้อย่างมีประสิทธิภาพมากขึ้นและลดความเสี่ยงของข้อผิดพลาดขณะรันไทม์ ดังนั้น มาเริ่มสำรวจโลกของ state machines ที่ปลอดภัยต่อประเภทข้อมูลด้วย TypeScript กันเถอะ!