本文 AI 產出,尚未審核

TypeScript 基本型別 - Union Type(聯合型別)

簡介

在日常的前端開發中,我們常會遇到「同一個變數可能會有不同型別」的情況,例如從 API 取得的資料可能是 stringnull,表單輸入可能是 numberundefined。如果僅用單一型別描述,TypeScript 會抱怨型別不相容,導致開發者必須自行加入大量的型別斷言(type assertion)或 any,失去型別安全的好處。

Union Type(聯合型別) 正是為了解決這類需求而設計的。它允許我們把多個型別「合併」成一個新型別,告訴編譯器「這個值可以是 A,也可以是 B,甚至是 C…」。透過 Union,我可以在保留型別檢查的同時,寫出更彈性且易於維護的程式碼。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你掌握 Union Type 的使用方式,並提供實務案例,讓你在日常開發中立刻受益。


核心概念

1. 基本語法

Union Type 使用直線符號 | 連接多個型別,語法如下:

type MyUnion = string | number | boolean;

上述 MyUnion 表示只能是 stringnumberboolean 其中之一。當我們宣告變數時,只要符合其中任一型別,即可通過編譯:

let value: MyUnion;

value = "hello";   // ✅ string
value = 42;        // ✅ number
value = true;      // ✅ boolean
// value = {a:1}; // ❌ TypeScript 會報錯

2. 與字面量型別結合

Union 常與字面量型別(Literal Types)結合,形成受限的選項集合,非常適合描述 UI 中的選單、狀態碼等:

type Direction = "up" | "down" | "left" | "right";

function move(dir: Direction) {
  console.log(`移動方向:${dir}`);
}

move("up");    // ✅
move("center"); // ❌ 編譯錯誤

3. 交叉型別(Intersection)與 Union 的差異

  • Union (A | B):值可以是 AB
  • Intersection (A & B):值同時必須符合 AB(即合併屬性)。
type A = { a: number };
type B = { b: string };
type C = A & B; // { a: number; b: string }
type D = A | B; // { a: number } | { b: string }

4. 型別守衛(Type Guard)

使用 Union 時,編譯器無法直接知道變數到底是哪一個型別。此時,我們需要型別守衛(如 typeofinstanceof、自訂型別保護函式)來縮窄型別,讓後續的操作安全無誤。

function format(value: string | number) {
  if (typeof value === "string") {
    // 進入此分支,value 被縮窄為 string
    return value.toUpperCase();
  } else {
    // 這裡 value 為 number
    return value.toFixed(2);
  }
}

5. 可辨識聯合型別(Discriminated Union)

當每個成員都有一個共同的「標記」屬性(通常是字串常量),我們可以利用它來做更直觀的分支判斷,這種模式稱為可辨識聯合型別

type Square = { kind: "square"; size: number };
type Circle = { kind: "circle"; radius: number };
type Shape = Square | Circle;

function area(shape: Shape): number {
  switch (shape.kind) {
    case "square":
      return shape.size ** 2;   // shape 被視為 Square
    case "circle":
      return Math.PI * shape.radius ** 2; // shape 被視為 Circle
  }
}

程式碼範例

範例 1️⃣:API 回傳可能為 null

type UserId = string | null;

function greet(id: UserId) {
  if (id === null) {
    console.log("訪客,歡迎光臨!");
  } else {
    console.log(`使用者 ${id},您好!`);
  }
}

greet("A123"); // 使用者 A123,您好!
greet(null);   // 訪客,歡迎光臨!

說明:利用 nullstring 的 Union,避免使用 any,同時保留型別安全。

範例 2️⃣:表單欄位可接受多種型別

type InputValue = string | number | boolean | undefined;

function normalize(value: InputValue): string {
  if (typeof value === "string") return value.trim();
  if (typeof value === "number") return value.toString();
  if (typeof value === "boolean") return value ? "true" : "false";
  return ""; // undefined 直接回傳空字串
}

說明normalize 透過 typeof 型別守衛,將不同型別的輸入統一轉成字串,常見於表單送出前的資料清理。

範例 3️⃣:可辨識聯合型別實作 UI 元件

type ButtonProps =
  | { type: "primary"; onClick: () => void; label: string }
  | { type: "secondary"; href: string; label: string }
  | { type: "icon"; icon: string; onClick: () => void };

function renderButton(props: ButtonProps) {
  switch (props.type) {
    case "primary":
      return `<button class="btn-primary" onclick="${props.onClick}">${props.label}</button>`;
    case "secondary":
      return `<a class="btn-secondary" href="${props.href}">${props.label}</a>`;
    case "icon":
      return `<button class="btn-icon" onclick="${props.onClick}"><i class="${props.icon}"></i></button>`;
  }
}

說明:每個子型別都有唯一的 type 標記,讓 switch 能正確推斷屬性,避免手動的 as 斷言。

範例 4️⃣:函式重載結合 Union

function combine(a: string, b: string): string;
function combine(a: number, b: number): number;
function combine(a: string | number, b: string | number): string | number {
  if (typeof a === "string" && typeof b === "string") {
    return a + b; // 字串相加
  }
  if (typeof a === "number" && typeof b === "number") {
    return a + b; // 數字相加
  }
  throw new Error("參數型別不匹配");
}

const r1 = combine("Hello, ", "World!"); // string
const r2 = combine(10, 20);               // number

說明:透過函式重載,我們可以在外部看到更具體的回傳型別,同時在實作內部使用 Union 進行型別縮窄。

範例 5️⃣:條件型別(Conditional Types)與 Union 的結合

type Flatten<T> = T extends (infer U)[] ? U : T;

type A = Flatten<string | number[]>; // 結果是 string | number

說明Flatten 會把陣列型別展平,若傳入的是 string | number[],最終得到的型別仍是 Union,展示了 Union 在進階型別操作中的彈性。


常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記縮窄型別 直接對 Union 成員使用特定屬性會產生錯誤,例如 value.toUpperCase() 在 `string number` 上。
過度使用 any 為了省事把 Union 改成 any,失去型別檢查。 儘量保留具體的 Union,必要時使用 unknown + 型別斷言。
字面量型別寫錯 "up""Down" 大小寫不一致會導致編譯錯誤。 建議使用 as constenum 定義字面量集合。
Union 互相重疊 `string "hello"會讓"hello"` 成為冗餘。
函式回傳過寬 只回傳 `string number,實際上永遠是 string`,造成使用者需額外檢查。

最佳實踐

  1. 盡量使用字面量型別 定義可接受的值集合,提升 IDE 自動完成與文件可讀性。
  2. 結合可辨識聯合型別,在大型狀態機或 UI 元件時,能讓 switch/if 分支更安全。
  3. 在 API 介面層 使用 null | undefined 與實際型別的 Union,避免在業務層寫大量防呆程式。
  4. 配合 readonlyas const,讓 Union 成員在編譯時成為不可變,防止意外改寫。
  5. 測試型別:使用 tsc --noEmittype-tests(例如 tsd)確保 Union 的行為符合預期。

實際應用場景

1. 前端表單驗證

在表單驗證函式中,欄位值可能是 stringnumbernullundefined。使用 Union 可以一次描述所有可能,並在驗證階段縮窄型別,減少重覆的防呆程式。

type FormValue = string | number | null | undefined;

function isRequired(value: FormValue): boolean {
  return value !== null && value !== undefined && value !== "";
}

2. Redux/NgRx 狀態管理

在 Redux 中,action.type 常使用字面量聯合型別,配合可辨識聯合型別描述 payload

type CounterAction =
  | { type: "increment"; amount: number }
  | { type: "decrement"; amount: number }
  | { type: "reset" };

function reducer(state: number, action: CounterAction): number {
  switch (action.type) {
    case "increment": return state + action.amount;
    case "decrement": return state - action.amount;
    case "reset":    return 0;
  }
}

3. 多媒體檔案處理

一個函式需要接受圖片檔案 (File) 或遠端 URL (string) 兩種來源,使用 Union 能一次處理:

type MediaSource = File | string;

function upload(source: MediaSource) {
  if (source instanceof File) {
    // 直接上傳檔案
  } else {
    // 先 fetch 再上傳
  }
}

4. API 客戶端 SDK

SDK 常提供「可選」的參數,例如 timeout 可以是 number"default"

type RequestOptions = {
  method: "GET" | "POST";
  timeout?: number | "default";
};

function request(url: string, opts: RequestOptions) {
  const timeout = opts.timeout === "default" ? 5000 : opts.timeout ?? 3000;
  // ...
}

總結

Union Type 是 TypeScript 中極為重要且實用的特性,讓變數可以同時接受多種型別,同時保留靜態型別檢查的優勢。透過 |、字面量型別、可辨識聯合型別以及型別守衛,我們能在開發過程中:

  • 減少 any 的使用,提升程式碼安全性。
  • 簡化 API 介面的描述,讓前後端契約更明確。
  • 在 UI、狀態管理或資料處理等場景,寫出彈性且易於維護的程式碼。

掌握了 Union 的基本語法與最佳實踐後,你就能在日常開發中自如地處理「多型別」的需求,並以更嚴謹的方式防止潛在的錯誤。祝你在 TypeScript 的旅程中,寫出更乾淨、更安全的程式碼!