Featured image of post 使用受歧視聯合的型別安全狀態轉換Featured image of post 使用受歧視聯合的型別安全狀態轉換

使用受歧視聯合的型別安全狀態轉換

利用聯合欄位(如「狀態」標籤)來清晰地分離業務條件並允許 TS 詳盡檢查。

在使用 TypeScript 的現代前端和後端開發中,確保狀態轉換期間的類型安全是建立健壯且無崩潰的應用程式的關鍵要素。在處理非同步 API 通訊(例如載入、成功和錯誤狀態之間的轉換)或複雜的使用者互動時,不明確的類型定義很容易導致意外的執行階段錯誤。

在本文中,我們將探索如何利用 TypeScript 中的受歧視聯合(也稱為標記聯合)的強大功能來安全、優雅且使用最少的樣板檔案來管理狀態。


1. 什麼是受歧視工會?

可判別聯合是 TypeScript 中的一種模式,它將多個不同的物件類型組合成一個聯合類型 (|),其中每個物件類型共用一個公共文字屬性 - 稱為「判別式」或「標籤」。

TypeScript 的編譯器可以在條件結構(如 ifswitch 語句)內讀取此標記,以自動縮小對應區塊內物件的類型範圍(稱為類型縮小)。

受歧視聯盟的關鍵要素:

  1. 多種物件類型組合成一個聯合。
  2. 每種類型中都存在共享屬性名稱(判別式)。
  3. 此共享屬性的類型是文字類型(特定的字串、數字或布林值)。

2. 簡單的方法:單一整體對象

在深入研究可區分聯合之前,我們先檢查一個反模式:使用包含所有可選欄位的單一物件表示。

// Anti-pattern: Optional properties for all possible states
interface FetchState<T> {
  isLoading: boolean;
  data?: T;
  error?: Error;
}

function renderState<T>(state: FetchState<T>) {
  if (state.isLoading) {
    return "Loading...";
  }

  // Even if isLoading is false, there is no guarantee that data or error is present.
  if (state.data) {
    // TypeScript requires a check or safe-navigation because 'data' is optional.
    return `Data: ${JSON.stringify(state.data)}`;
  }

  if (state.error) {
    return `Error: ${state.error.message}`;
  }

  return "Unknown state";
}

這種方法的缺點:

  • 我們可以輕鬆地設定不可能的狀態(例如,同時定義 dataerror ,或當 isLoading 為 false 時兩者都是 undefined )。
  • 開發人員必須不斷使用可選連結 (?.) 或非空斷言運算子 (!) 來存取數據,這破壞了靜態類型檢查的目的。

3. 實施歧視性工會

讓我們使用一組嚴格定義的狀態重新設計我們的狀態表示,並透過聯合類型組合。

// Define separate interfaces for every distinct status
interface IdleState {
  type: 'idle';
}

interface LoadingState {
  type: 'loading';
}

interface SuccessState<T> {
  type: 'success';
  data: T;
}

interface ErrorState {
  type: 'error';
  error: Error;
}

// Combine them into a union type
type FetchState<T> = IdleState | LoadingState | SuccessState<T> | ErrorState;

在此模型中,type 屬性是我們的判別式。讓我們看看現在渲染這個狀態是多麼容易:

function renderState<T>(state: FetchState<T>): string {
  switch (state.type) {
    case 'idle':
      return 'Waiting...';
    case 'loading':
      return 'Loading...';
    case 'success':
      // The compiler guarantees that 'data' is present here. No optional chaining needed.
      return `Data: ${JSON.stringify(state.data)}`;
    case 'error':
      // The compiler guarantees that 'error' is present here.
      return `Error occurred: ${state.error.message}`;
  }
}

4. 安全第一:詳盡檢查

使用受歧視聯合的最大好處之一是能夠利用詳盡性檢查。當您新增的狀態類型時,您希望編譯器在您忘記處理它時發出警告。

我們可以使用 never 類型來實現這一點:

function assertNever(value: never): never {
  throw new Error(`Unhandled state: ${JSON.stringify(value)}`);
}

function renderStateWithGuard<T>(state: FetchState<T>): string {
  switch (state.type) {
    case 'idle':
      return 'Waiting...';
    case 'loading':
      return 'Loading...';
    case 'success':
      return `Data: ${state.data}`;
    case 'error':
      return `Error: ${state.error.message}`;
    default:
      // If we add 'canceling' state in the future and forget to add a case for it,
      // TypeScript will raise a compile-time error here because 'state' cannot be reduced to 'never'.
      return assertNever(state);
  }
}

結論

透過在程式碼庫中採用受歧視聯合,您將獲得:

  1. 零不可能狀態:結構上禁止狀態的不可能變化。
  2. 可預測的資料存取:消除防禦性程式模式(例如非空斷言覆蓋)。
  3. 編譯時安心:詳盡檢查在重構過程中充當內建的安全網。

無論您是設計 React 減速器操作、Redux 狀態還是 API 訊息有效負載,可區分聯合都是您應該採用的核心 TypeScript 模式。