iT邦幫忙

2024 iThome 鐵人賽

DAY 4
0
Modern Web

前進React 生態系 : 技術應用與概念解析系列 第 4

Day 04 - 掌握 TypeScript Discriminated Unions:提升 React 組件的靈活性與安全性

  • 分享至 

  • xImage
  •  

什麼是 Discriminated Unions

先看範例:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

function area(s: Shape) {
  if (s.kind === "circle") {
    return Math.PI * s.radius * s.radius;
  } else if (s.kind === "square") {
    return s.x * s.x;
  } else {
    return (s.x * s.y) / 2;
  }
}

在這個範例中,Shape 是一個 Union Type。每個型別都有辨識屬性 kind,TypeScript 會根據這個屬性來區分型別。例如,當 kind"circle" 時,TypeScript 可以推斷 s 一定有 radius 屬性,而不會有其他不相關的屬性。

透過 if 條件,TypeScript 可以縮小型別範圍,當判斷條件 s.kind === "circle" 為真時,TypeScript 會將 s 的型別縮小到 circle,這樣就可以安全地訪問 radius 屬性。

當然除了 ifswitch 也有同樣的效果。

Type Guards 和 Narrowing

Type Guards : 是指在透過特定的邏輯判斷來縮小變數的型別範圍,有時也會搭配 typeof , instanceof , in 等關鍵字使用。
Narrowing : 是指將變數的型別從更廣泛的範圍縮小到具體型別的過程,常搭配 Type Guards 使用。

Discriminated Unions 使用時機

Discriminated Unions 適合在處理多種可能的狀態或多個不同類型的情境時使用。特別是當我們有多個型別之間有部份差異,但還是共享某些共同屬性時。常用於下列情境:

  • API 回傳狀態:根據不同的 API 回傳狀態,可能會有不同的結構和資料。
  • React Component Props:根據不同的 props 的值,會渲染不同的 UI 或使用不同的邏輯。

以 React Component Props 為例

這邊我寫一個 UserCard 的 Component,總共分成三種情況:

  1. role 為 "admin" 時,組件需要顯示 permissions,並且該屬性是必須的。
  2. role 為 "user" 時,組件需要顯示 email,該屬性也是必須的。
  3. role 為 "guest" 時,組件不需要顯示任何額外屬性。

範例一 : 使用可選參數的 UserCard 組件

type UserCardProps = {
  name: string;
  role: "user" | "admin" | "guest";
  permissions?: string[]; // 只有 admin 才有這個屬性,但這裡是可選的
  email?: string; // 只有 user 才有 email,但這裡也是可選的
};

export default function UserCard({ props }: { props: UserCardProps }) {
  const { name, role, permissions, email } = props;

  if (role === "admin") {
    return (
      <div>
        <h2>{name} (Admin)</h2>
        <p>Permissions: {permissions?.join(", ")}</p>
      </div>
    );
  }

  if (role === "user") {
    return (
      <div>
        <h2>{name} (User)</h2>
        <p>Email: {email}</p>
      </div>
    );
  }

  return (
    <div>
      <h2>{name} (Guest)</h2>
    </div>
  );
}

這個範例的問題:

  • 型別安全性不足:即使 role'guest',有可能會意外傳入 permissionsemail
  • 潛在的錯誤:在使用 permissions?.join(", ")email 的時候,如果這些屬性沒有被傳入,結果將會是 undefined,這可能會導致顯示錯誤和其他問題。

範例二:使用 Discriminated Unions 的 UserCard 組件

type AdminProps = {
  role: "admin";
  permissions: string[];
};

type UserProps = {
  role: "user";
  email: string;
};

type GuestProps = {
  role: "guest";
};

type UserCardProps = {
  name: string;
} & (AdminProps | UserProps | GuestProps);

export default function UserCard({ props }: { props: UserCardProps }) {
  const { name, role } = props; 
  // 如果在這邊直接取 permissions 或 email 會出現錯誤,因為 TypeScript 知道這些屬性不是通用的

  if (role === "admin") {
    const { permissions } = props; 
    // TypeScript 確保當 role 是 "admin" 時,permissions 一定存在
    return (
      <div>
        <h2>{name} (Admin)</h2>
        <p>Permissions: {permissions.join(", ")}</p>
      </div>
    );
  }

  if (role === "user") {
    const { email } = props; 
    // 這邊也是一樣,當 role 是 "user" 時,email 一定存在
    return (
      <div>
        <h2>{name} (User)</h2>
        <p>Email: {email}</p>
      </div>
    );
  }

  return (
    <div>
      <h2>{name} (Guest)</h2>
    </div>
  );
}

參考資料:
https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions
https://www.typescriptlang.org/docs/handbook/2/narrowing.html
https://www.totaltypescript.com/discriminated-unions-are-a-devs-best-friend


上一篇
Day 03 - 更複雜的泛型,那些好用的關鍵字和 Utility Types
下一篇
Day 05 - 掌握 ComponentProps:讓 React 元件型別更輕鬆
系列文
前進React 生態系 : 技術應用與概念解析22
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言