探索 TypeScript 可辨識聯合,這是一個用於建立強大且類型安全的狀態機的強大工具。 學習如何定義狀態、處理轉換,並利用 TypeScript 的類型系統來提高程式碼的可靠性。
TypeScript 可辨識聯合:建立類型安全的狀態機
在軟體開發領域,有效地管理應用程式狀態至關重要。狀態機為建模複雜的有狀態系統提供了一個強大的抽象,確保可預測的行為並簡化關於系統邏輯的推理。TypeScript 憑藉其強大的類型系統,提供了一種極好的機制,可以使用可辨識聯合(也稱為標記聯合或代數資料類型)來建立類型安全的狀態機。
什麼是可辨識聯合?
可辨識聯合是一種表示可以有多種不同類型的值的類型。這些類型中的每一種(稱為聯合的成員)都共享一個公共的、不同的屬性,稱為辨識符或標記。此辨識符允許 TypeScript 精確地確定當前哪個聯合成員處於活動狀態,從而實現強大的類型檢查和自動完成。
可以把它想像成交通號誌。它可以處於三種狀態之一:紅色、黃色或綠色。`color` 屬性充當辨識符,告訴我們燈光處於哪種狀態。
為什麼使用可辨識聯合來構建狀態機?
在使用 TypeScript 構建狀態機時,可辨識聯合帶來了幾個主要優點:
- 類型安全:編譯器可以驗證所有可能的狀態和轉換都得到正確處理,從而防止與意外狀態轉換相關的執行階段錯誤。這在大型、複雜的應用程式中特別有用。
- 窮盡性檢查:TypeScript 可以確保您的程式碼處理狀態機的所有可能狀態,如果在條件陳述式或 switch 案例中遺漏了某個狀態,它會在編譯時發出警示。這有助於防止意外行為並使您的程式碼更健壯。
- 提高可讀性:可辨識聯合清楚地定義了系統的可能狀態,使程式碼更易於理解和維護。狀態的顯式表示增強了程式碼的清晰度。
- 增強的程式碼完成:TypeScript 的 intellisense 根據當前狀態提供智慧程式碼完成建議,從而減少了錯誤的可能性並加快了開發速度。
使用可辨識聯合定義狀態機
讓我們用一個實際的例子來說明如何使用可辨識聯合來定義狀態機:訂單處理系統。訂單可以處於以下狀態:待處理、處理中、已出貨和已送達。
步驟 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:建立可辨識聯合類型
接下來,我們使用 `|`(聯合)運算符組合這些單獨的類型來建立可辨識聯合。
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; // 沒有狀態變更
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; // 或拋出錯誤
}
}
說明
- `processOrder` 函數將當前 `OrderState` 和一個 `Action` 作為輸入。
- 它使用 `switch` 陳述式根據 `state.type` 辨識符確定當前狀態。
- 在每個 `case` 內,它檢查 `action.type` 以確定是否觸發了有效的轉換。
- 如果找到有效的轉換,它會傳回一個具有適當 `type` 和資料的新狀態物件。
- 如果未找到有效的轉換,它會傳回當前狀態(或拋出錯誤,具體取決於所需的行為)。
- 包含 `default` 案例是為了完整性,並且理想情況下,由於 TypeScript 的窮盡性檢查,永遠不應達到。
利用窮盡性檢查
TypeScript 的窮盡性檢查是一項強大的功能,可確保您處理狀態機中的所有可能狀態。如果您向 `OrderState` 聯合新增一個新狀態,但忘記更新 `processOrder` 函數,TypeScript 將標記一個錯誤。
若要啟用窮盡性檢查,可以使用 `never` 類型。在 switch 陳述式的 `default` 案例內,將狀態指派給 `never` 類型的變數。
function processOrder(state: OrderState, action: Action): OrderState {
switch (state.type) {
// ... (先前的案例) ...
default:
const _exhaustiveCheck: never = state;
return _exhaustiveCheck; // 或拋出錯誤
}
}
如果 `switch` 陳述式處理所有可能的 `OrderState` 值,則 `_exhaustiveCheck` 變數將為 `never` 類型,並且程式碼將會編譯。但是,如果您向 `OrderState` 聯合新增一個新狀態,但忘記在 `switch` 陳述式中處理它,則 `_exhaustiveCheck` 變數將為不同的類型,並且 TypeScript 將拋出編譯時錯誤,從而提醒您缺少案例。
實際範例和應用
可辨識聯合適用於簡單訂單處理系統以外的各種情況:
- UI 狀態管理:建模 UI 元件的狀態(例如,載入中、成功、錯誤)。
- 網路請求處理:表示網路請求的不同階段(例如,初始、進行中、成功、失敗)。
- 表單驗證:追蹤表單欄位的有效性和整體表單狀態。
- 遊戲開發:定義遊戲角色或物件的不同狀態。
- 驗證流程:管理使用者驗證狀態(例如,已登入、已登出、待驗證)。
範例:UI 狀態管理
讓我們考慮一個管理從 API 提取資料的 UI 元件狀態的簡單範例。我們可以定義以下狀態:
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;
}
}
此範例示範了如何使用可辨識聯合來有效地管理 UI 元件的不同狀態,確保根據當前狀態正確呈現 UI。`renderUI` 函數適當地處理每個狀態,從而提供了一種清晰且類型安全的方式來管理 UI。
使用可辨識聯合的最佳實務
若要在您的 TypeScript 專案中有效地利用可辨識聯合,請考慮以下最佳實務:
- 選擇有意義的辨識符名稱:選擇清楚指示屬性用途的辨識符名稱(例如,`type`、`state`、`status`)。
- 保持狀態資料最小化:每個狀態應僅包含與該特定狀態相關的資料。避免在狀態中儲存不必要的資料。
- 使用窮盡性檢查:始終啟用窮盡性檢查以確保您處理所有可能的狀態。
- 考慮使用狀態管理程式庫:對於複雜的狀態機,請考慮使用專用的狀態管理程式庫(如 XState),該程式庫提供諸如狀態圖、階層狀態和平行狀態等進階功能。但是,對於較簡單的情況,可辨識聯合可能就足夠了。
- 記錄您的狀態機:清楚地記錄狀態機的不同狀態、轉換和操作,以提高可維護性和協作性。
進階技術
條件類型
條件類型可以與可辨識聯合組合使用,以建立更強大、更靈活的狀態機。例如,您可以使用條件類型來根據當前狀態為函數定義不同的傳回類型。
function getData(state: UIState): T | undefined {
if (state.type === "success") {
return state.data;
}
return undefined;
}
此函數使用一個簡單的 `if` 陳述式,但可以使用條件類型使其更健壯,以確保始終傳回特定的類型。
公用程式類型
TypeScript 的公用程式類型(例如 `Extract` 和 `Omit`)在處理可辨識聯合時非常有用。`Extract` 允許您根據條件從聯合類型中提取特定的成員,而 `Omit` 允許您從類型中刪除屬性。
// 從 UIState 聯合中提取「成功」狀態
type SuccessState = Extract, { type: "success" }>;
// 從 Error 介面中省略 'message' 屬性
type ErrorWithoutMessage = Omit;
各個產業的真實世界範例
可辨識聯合的強大功能擴展到各個產業和應用領域:
- 電子商務 (全球):在一個全球電子商務平台中,可以使用可辨識聯合來表示訂單狀態,處理諸如「待付款」、「處理中」、「已出貨」、「運輸中」、「已送達」和「已取消」等狀態。這確保了跨不同國家/地區的正確追蹤和溝通,因為這些國家/地區的運輸物流各不相同。
- 金融服務 (國際銀行):管理諸如「待授權」、「已授權」、「處理中」、「已完成」、「失敗」等交易狀態至關重要。可辨識聯合提供了一種強大的方式來處理這些狀態,同時遵守不同的國際銀行法規。
- 醫療保健 (遠端患者監控):使用諸如「正常」、「警告」、「危急」等狀態來表示患者的健康狀況,可以實現及時干預。在全球分佈的醫療保健系統中,可辨識聯合可以確保一致的資料解釋,而無論位置如何。
- 物流 (全球供應鏈):跨國際邊境追蹤貨物狀態涉及複雜的工作流程。諸如「海關清關」、「運輸中」、「在配送中心」、「已送達」等狀態非常適合可辨識聯合實作。
- 教育 (線上學習平台):使用諸如「已註冊」、「進行中」、「已完成」、「已放棄」等狀態來管理課程註冊狀態,可以提供簡化的學習體驗,並可以適應世界各地不同的教育系統。
結論
TypeScript 可辨識聯合提供了一種強大且類型安全的方式來建立狀態機。透過清楚地定義可能的狀態和轉換,您可以建立更強大、更易於維護和理解的程式碼。類型安全、窮盡性檢查和增強的程式碼完成的組合使可辨識聯合成為處理複雜狀態管理的任何 TypeScript 開發人員的寶貴工具。在您的下一個專案中採用可辨識聯合,並親身體驗類型安全狀態管理的好處。正如我們透過從電子商務到醫療保健,以及從物流到教育的各種範例所展示的那樣,透過可辨識聯合實現類型安全狀態管理的原則具有普遍的適用性。
無論您是在建立一個簡單的 UI 元件還是複雜的企業應用程式,可辨識聯合都可以幫助您更有效地管理狀態並降低執行階段錯誤的風險。因此,請深入研究並使用 TypeScript 探索類型安全狀態機的世界!