TypeScript 基本型別 - Union Type(聯合型別)
簡介
在日常的前端開發中,我們常會遇到「同一個變數可能會有不同型別」的情況,例如從 API 取得的資料可能是 string 或 null,表單輸入可能是 number 或 undefined。如果僅用單一型別描述,TypeScript 會抱怨型別不相容,導致開發者必須自行加入大量的型別斷言(type assertion)或 any,失去型別安全的好處。
Union Type(聯合型別) 正是為了解決這類需求而設計的。它允許我們把多個型別「合併」成一個新型別,告訴編譯器「這個值可以是 A,也可以是 B,甚至是 C…」。透過 Union,我可以在保留型別檢查的同時,寫出更彈性且易於維護的程式碼。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你掌握 Union Type 的使用方式,並提供實務案例,讓你在日常開發中立刻受益。
核心概念
1. 基本語法
Union Type 使用直線符號 | 連接多個型別,語法如下:
type MyUnion = string | number | boolean;
上述 MyUnion 表示只能是 string、number 或 boolean 其中之一。當我們宣告變數時,只要符合其中任一型別,即可通過編譯:
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):值可以是 A 或 B。 - Intersection (
A & B):值同時必須符合 A 與 B(即合併屬性)。
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 時,編譯器無法直接知道變數到底是哪一個型別。此時,我們需要型別守衛(如 typeof、instanceof、自訂型別保護函式)來縮窄型別,讓後續的操作安全無誤。
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); // 訪客,歡迎光臨!
說明:利用
null與string的 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 const 或 enum 定義字面量集合。 |
| Union 互相重疊 | `string | "hello"會讓"hello"` 成為冗餘。 |
| 函式回傳過寬 | 只回傳 `string | number,實際上永遠是 string`,造成使用者需額外檢查。 |
最佳實踐
- 盡量使用字面量型別 定義可接受的值集合,提升 IDE 自動完成與文件可讀性。
- 結合可辨識聯合型別,在大型狀態機或 UI 元件時,能讓
switch/if分支更安全。 - 在 API 介面層 使用
null | undefined與實際型別的 Union,避免在業務層寫大量防呆程式。 - 配合
readonly或as const,讓 Union 成員在編譯時成為不可變,防止意外改寫。 - 測試型別:使用
tsc --noEmit或type-tests(例如tsd)確保 Union 的行為符合預期。
實際應用場景
1. 前端表單驗證
在表單驗證函式中,欄位值可能是 string、number、null 或 undefined。使用 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 的旅程中,寫出更乾淨、更安全的程式碼!