עברית

גלו את האיחודים המובחנים של TypeScript, כלי רב-עוצמה לבניית מכונות מצבים חזקות ובטוחות-טיפוסים. למדו כיצד להגדיר מצבים, לטפל במעברים, ולמנף את מערכת הטיפוסים של TypeScript לאמינות קוד גבוהה יותר.

Discriminated Unions ב-TypeScript: בניית מכונות מצבים בטוחות-טיפוס

בעולם פיתוח התוכנה, ניהול מצב האפליקציה באופן יעיל הוא קריטי. מכונות מצבים מספקות הפשטה רבת עוצמה למדילת מערכות מורכבות בעלות מצב, ומבטיחות התנהגות צפויה ומפשטות את ההיגיון הלוגי של המערכת. TypeScript, עם מערכת הטיפוסים החזקה שלה, מציעה מנגנון פנטסטי לבניית מכונות מצבים בטוחות-טיפוסים באמצעות איחודים מובחנים (הידועים גם כ-tagged unions או algebraic data types).

מהם איחודים מובחנים (Discriminated Unions)?

איחוד מובחן הוא טיפוס המייצג ערך שיכול להיות אחד מכמה טיפוסים שונים. כל אחד מהטיפוסים הללו, הידועים כחברים באיחוד, חולק מאפיין משותף ומובחן הנקרא המבחין (discriminant) או התג (tag). מבחין זה מאפשר ל-TypeScript לקבוע במדויק איזה חבר באיחוד פעיל כרגע, ובכך מאפשר בדיקת טיפוסים חזקה והשלמה אוטומטית.

חשבו על זה כמו רמזור. הוא יכול להיות באחד משלושה מצבים: אדום, צהוב או ירוק. מאפיין ה-'color' משמש כמבחין, ואומר לנו בדיוק באיזה מצב הרמזור נמצא.

למה להשתמש ב-Discriminated Unions עבור מכונות מצבים?

איחודים מובחנים מביאים עימם מספר יתרונות מרכזיים בעת בניית מכונות מצבים ב-TypeScript:

הגדרת מכונת מצבים עם איחודים מובחנים

בואו נדגים כיצד להגדיר מכונת מצבים באמצעות איחודים מובחנים עם דוגמה מעשית: מערכת עיבוד הזמנות. הזמנה יכולה להיות במצבים הבאים: Pending, Processing, Shipped, ו-Delivered.

שלב 1: הגדרת טיפוסי המצב

ראשית, אנו מגדירים את הטיפוסים הנפרדים עבור כל מצב. לכל טיפוס יהיה מאפיין `type` שישמש כמבחין, יחד עם כל נתון ספציפי למצב.


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: יצירת טיפוס האיחוד המובחן

לאחר מכן, ניצור את האיחוד המובחן על ידי שילוב הטיפוסים הנפרדים באמצעות האופרטור `|` (union).


type OrderState = Pending | Processing | Shipped | Delivered;

כעת, `OrderState` מייצג ערך שיכול להיות `Pending`, `Processing`, `Shipped`, או `Delivered`. מאפיין ה-`type` בתוך כל מצב משמש כמבחין, ומאפשר ל-TypeScript להבדיל ביניהם.

טיפול במעברי מצב

כעת, לאחר שהגדרנו את מכונת המצבים שלנו, אנו זקוקים למנגנון למעבר בין מצבים. בואו ניצור פונקציה `processOrder` שמקבלת את המצב הנוכחי ופעולה כקלט ומחזירה את המצב החדש.


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; // No state change

    case "processing":
      if (action.type === "shipOrder") {
        return {
          type: "shipped",
          orderId: state.orderId,
          trackingNumber: action.payload.trackingNumber,
        };
      }
      return state; // No state change

    case "shipped":
      if (action.type === "deliverOrder") {
        return {
          type: "delivered",
          orderId: state.orderId,
          deliveryDate: new Date(),
        };
      }
      return state; // No state change

    case "delivered":
      // Order is already delivered, no further actions
      return state;

    default:
      // This should never happen due to exhaustiveness checking
      return state; // Or throw an error
  }
}

הסבר

מינוף בדיקת ממצה (Exhaustiveness Checking)

בדיקת הממצה של TypeScript היא תכונה רבת עוצמה המוודאת שאתם מטפלים בכל המצבים האפשריים במכונת המצבים שלכם. אם תוסיפו מצב חדש לאיחוד `OrderState` אבל תשכחו לעדכן את הפונקציה `processOrder`, TypeScript תסמן שגיאה.

כדי לאפשר בדיקת ממצה, אתם יכולים להשתמש בטיפוס `never`. בתוך מקרה ה-`default` של משפט ה-switch שלכם, הקצו את המצב למשתנה מסוג `never`.


function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    // ... (previous cases) ...

    default:
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck; // Or throw an error
  }
}

אם משפט ה-`switch` מטפל בכל הערכים האפשריים של `OrderState`, המשתנה `_exhaustiveCheck` יהיה מסוג `never` והקוד יתקמפל. עם זאת, אם תוסיפו מצב חדש לאיחוד `OrderState` ותשכחו לטפל בו במשפט ה-`switch`, המשתנה `_exhaustiveCheck` יהיה מטיפוס אחר, ו-TypeScript תזרוק שגיאת קומפילציה, ותתריע לכם על המקרה החסר.

דוגמאות ויישומים מעשיים

איחודים מובחנים ישימים במגוון רחב של תרחישים מעבר למערכות עיבוד הזמנות פשוטות:

דוגמה: ניהול מצב ממשק משתמש (UI)

בואו נבחן דוגמה פשוטה של ניהול מצב של רכיב UI אשר מביא נתונים מ-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 

Click the button to load data.

; case "loading": return

Loading...

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

Error: {state.message}

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

דוגמה זו מדגימה כיצד ניתן להשתמש באיחודים מובחנים כדי לנהל ביעילות את המצבים השונים של רכיב UI, ולהבטיח שה-UI מוצג כראוי בהתבסס על המצב הנוכחי. הפונקציה `renderUI` מטפלת בכל מצב כראוי, ומספקת דרך ברורה ובטוחת-טיפוס לניהול ה-UI.

שיטות עבודה מומלצות לשימוש ב-Discriminated Unions

כדי להשתמש ביעילות באיחודים מובחנים בפרויקטי ה-TypeScript שלכם, שקלו את שיטות העבודה המומלצות הבאות:

טכניקות מתקדמות

טיפוסים מותנים (Conditional Types)

ניתן לשלב טיפוסים מותנים עם איחודים מובחנים כדי ליצור מכונות מצבים חזקות וגמישות עוד יותר. לדוגמה, אתם יכולים להשתמש בטיפוסים מותנים כדי להגדיר טיפוסי החזרה שונים עבור פונקציה בהתבסס על המצב הנוכחי.


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

פונקציה זו משתמשת במשפט `if` פשוט אך יכולה להיות חזקה יותר באמצעות טיפוסים מותנים כדי להבטיח שתמיד יוחזר טיפוס ספציפי.

טיפוסי עזר (Utility Types)

טיפוסי העזר של TypeScript, כגון `Extract` ו-`Omit`, יכולים להיות מועילים בעבודה עם איחודים מובחנים. `Extract` מאפשר לכם לחלץ חברים ספציפיים מטיפוס איחוד בהתבסס על תנאי, בעוד `Omit` מאפשר לכם להסיר מאפיינים מטיפוס.


// Extract the "success" state from the UIState union
type SuccessState = Extract, { type: "success" }>;

// Omit the 'message' property from the Error interface
type ErrorWithoutMessage = Omit;

דוגמאות מהעולם האמיתי בתעשיות שונות

העוצמה של איחודים מובחנים מתפרשת על פני תעשיות ותחומי יישום מגוונים:

סיכום

האיחודים המובחנים של TypeScript מספקים דרך חזקה ובטוחת-טיפוס לבניית מכונות מצבים. על ידי הגדרה ברורה של המצבים והמעברים האפשריים, אתם יכולים ליצור קוד חזק יותר, קל לתחזוקה ומובן יותר. השילוב של בטיחות טיפוסים, בדיקת ממצה והשלמת קוד משופרת הופך את האיחודים המובחנים לכלי שלא יסולא בפז עבור כל מפתח TypeScript העוסק בניהול מצבים מורכב. אמצו את האיחודים המובחנים בפרויקט הבא שלכם ותחוו את היתרונות של ניהול מצבים בטוח-טיפוס ממקור ראשון. כפי שהראינו עם דוגמאות מגוונות ממסחר אלקטרוני ועד בריאות, ומלוגיסטיקה ועד חינוך, העיקרון של ניהול מצבים בטוח-טיפוס באמצעות איחודים מובחנים הוא ישים באופן אוניברסלי.

בין אם אתם בונים רכיב UI פשוט או יישום ארגוני מורכב, איחודים מובחנים יכולים לעזור לכם לנהל מצבים בצורה יעילה יותר ולהפחית את הסיכון לשגיאות זמן ריצה. אז, צללו פנימה וחקרו את עולם מכונות המצבים בטוחות-הטיפוס עם TypeScript!