TypeScript 條件型別 (Conditional Types)

在程式設計中,我們習慣用 if-else 或三元運算子 condition ? true : false 來根據條件決定

而在 TypeScript 中,我們也需要類似的機制,但這次我們要決定的是型別。這就是「條件型別 (Conditional Types)」。

語法與運作原理

語法跟 JavaScript 的三元運算子一模一樣:

T extends U ? X : Y

白話文翻譯:如果型別 T 可以指派給型別 U(也就是 TU 的子集合或相容),那麼結果型別就是 X,否則就是 Y

簡單範例

type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>; // true (因為 "hello" 是 string)
type B = IsString<123>; // false (因為 123 不是 string)

這看起來很簡單,但它是許多高階型別操作的基礎。

分配條件型別 (Distributive Conditional Types)

這是條件型別中最容易讓人困惑,但也最強大的特性。

當你把一個「聯合型別 (Union Type)」傳入條件型別時,它會自動拆開並且一個一個處理

// 定義一個把它變成陣列的工具
type ToArray<T> = T extends any ? T[] : never;

// 當我們傳入 string | number 時,TypeScript 會這樣拆解:
// 1. 先算 ToArray<string> -> 結果是 string[]
// 2. 再算 ToArray<number> -> 結果是 number[]
// 3. 最後把結果合併 -> string[] | number[]

type Result = ToArray<string | number>;
// Result 是 string[] | number[]

這就像是 map 函式一樣,遍歷了聯合型別中的每一個成員。

如何阻止分配行為?

有時候我們不想要這種行為,我們希望把 string | number 當成一個整體來看待。解法是用 [] 把泛型包起來:

// 在 extends 兩邊都加上 []
type ToArrayStrict<T> = [T] extends [any] ? T[] : never;

type Result = ToArrayStrict<string | number>;
// Result 變成 (string | number)[]
// 也就是一個陣列,裡面可以混裝字串和數字

infer 關鍵字:型別中的「樣式比對」

infer 關鍵字只能在 extends條件子句中使用。你可以把它想像成一個「變數宣告」或者「佔位符」。

這就像也是在告訴 TypeScript:「我不知道這部分的型別是什麼,但我先把它叫做 R(或其他名字),如果型別比對成功了,你就把抓到的這個 R 給我用」。

實戰:推論函式的回傳值 (ReturnType)

這是最經典的例子。我們想知道一個函式到底回傳什麼型別。

// 解析:
// 1. T extends (...args: any[]) => infer R
//    -> 檢查 T 是不是一個函式?
//    -> 如果是,那它的回傳值型別,我暫時把它叫做 R (infer R)
// 2. ? R : never
//    -> 如果比對成功,最終結果就是這個 R
//    -> 如果失敗(T 根本不是函式),結果就是 never

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { name: '小明', age: 25 };
}

// 自動抓出 { name: string; age: number }
type User = MyReturnType<typeof getUser>;

實戰:推論 Promise 其中的型別

我們常遇到 Promise<string>,但我們想要裡面的 string

// 檢查 T 是不是 Promise?
// 如果是,把它裡面包的東西叫做 U
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;

type Data = UnpackPromise<Promise<string>>; // string
type Num = UnpackPromise<number>; // number (不是 Promise,回傳原本的 T)

內建的條件型別

TypeScript 已經幫我們寫好很多常用的條件型別了:

Exclude<T, U> (排除)

從 T 中「剔除」掉 U。

// 實作原理:如果 T 是 U,就丟掉 (never),否則保留 (T)
type Exclude<T, U> = T extends U ? never : T;

type Status = 'success' | 'error' | 'loading';
type NonLoading = Exclude<Status, 'loading'>;
// 結果:'success' | 'error'

Extract<T, U> (提取)

從 T 中「選出」 U。

type Extract<T, U> = T extends U ? T : never;

type Common = Extract<'a' | 'b', 'a' | 'c'>;
// 結果:'a'

NonNullable<T> (非空)

剔除 null 和 undefined。

type NonNullable<T> = T extends null | undefined ? never : T;

總結

條件型別是 TypeScript 用來寫「型別邏輯」的核心工具。

  1. 基本語法T extends U ? X : Y
  2. 分配特性:遇到 Union Type 會自動拆開處理 (Distributive)。
  3. infer 推論:用來在條件判斷時「抓取」局部的型別資訊。
  4. 內建工具:熟悉 Exclude, Extract 這些工具,它們都是用條件型別寫成的。

掌握了這章,你就能看懂大部分複雜的 TypeScript 原始碼宣告了!