TypeScript 條件型別 (Conditional Types)
條件型別 (Conditional Types) 允許我們根據條件選擇型別。它類似於 JavaScript 的三元運算子,但用於型別層級的操作。
基本語法
T extends U ? X : Y
如果 T 可以指派給 U,則結果為 X,否則為 Y。
基本範例
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'hello'>; // true
type IsArray<T> = T extends any[] ? true : false;
type D = IsArray<number[]>; // true
type E = IsArray<string>; // false
type F = IsArray<[1, 2, 3]>; // true
型別推論 (infer)
使用 infer 關鍵字可以在條件型別中推論型別:
// 取得函式的回傳型別
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet(): string {
return 'hello';
}
type GreetReturn = MyReturnType<typeof greet>; // string
推論陣列元素型別
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type A = ArrayElement<number[]>; // number
type B = ArrayElement<string[]>; // string
type C = ArrayElement<(number | string)[]>; // number | string
推論函式參數
type FirstParameter<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
function example(name: string, age: number) {}
type First = FirstParameter<typeof example>; // string
推論 Promise 的值
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
type C = Awaited<string>; // string
分配條件型別 (Distributive Conditional Types)
當條件型別作用於聯合型別時,會自動分配到每個成員:
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string>; // string[]
type B = ToArray<number>; // number[]
type C = ToArray<string | number>; // string[] | number[]
避免分配
使用元組包裝可以避免分配行為:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type A = ToArrayNonDist<string | number>; // (string | number)[]
內建條件型別
TypeScript 提供了幾個內建的條件型別:
Exclude<T, U>
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<'a' | 'b' | 'c', 'a'>; // "b" | "c"
type B = Exclude<string | number, string>; // number
Extract<T, U>
type Extract<T, U> = T extends U ? T : never;
type A = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // "a" | "b"
type B = Extract<string | number, string>; // string
NonNullable<T>
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null | undefined>; // string
實用條件型別
取得物件的值型別
type ValueOf<T> = T[keyof T];
interface User {
id: number;
name: string;
active: boolean;
}
type UserValue = ValueOf<User>; // number | string | boolean
過濾物件屬性
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
active: boolean;
}
type StringProps = FilterByType<Mixed, string>;
// { name: string; email: string }
type NumberProps = FilterByType<Mixed, number>;
// { age: number }
函式重載解析
type OverloadedReturnType<T> = T extends {
(...args: any[]): infer R1;
(...args: any[]): infer R2;
(...args: any[]): infer R3;
}
? R1 | R2 | R3
: T extends (...args: any[]) => infer R
? R
: never;
遞迴條件型別
TypeScript 4.1+ 支援遞迴條件型別:
深度展開 Promise
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type A = DeepAwaited<Promise<Promise<Promise<string>>>>; // string
深度 Readonly
type DeepReadonly<T> = T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
interface NestedObj {
a: {
b: {
c: string;
};
};
}
type ReadonlyNested = DeepReadonly<NestedObj>;
// 所有層級都變成 readonly
攤平陣列
type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;
type A = Flatten<number[]>; // number
type B = Flatten<number[][]>; // number
type C = Flatten<number[][][]>; // number
type D = Flatten<string>; // string
進階範例
路徑型別
type PathKeys<T, D extends number = 10> = [D] extends [never]
? never
: T extends object
? {
[K in keyof T]: K extends string ? `${K}` | `${K}.${PathKeys<T[K], Prev[D]>}` : never;
}[keyof T]
: never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
interface User {
name: string;
address: {
city: string;
country: string;
};
}
type UserPaths = PathKeys<User>;
// "name" | "address" | "address.city" | "address.country"
型別斷言輔助
type AssertEqual<T, U> =
(<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2 ? true : false;
type Test1 = AssertEqual<string, string>; // true
type Test2 = AssertEqual<string, number>; // false
聯合轉交集
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void
? I
: never;
type A = UnionToIntersection<{ a: string } | { b: number }>;
// { a: string } & { b: number }
型別守衛與條件型別
type IsNullable<T> = null extends T ? true : false;
type A = IsNullable<string | null>; // true
type B = IsNullable<string>; // false
// 建立非空版本
type EnsureNonNullable<T> = IsNullable<T> extends true ? NonNullable<T> : T;
條件型別的實際應用
API 回應處理
type ApiResponse<T> = T extends void ? { success: boolean } : { success: boolean; data: T };
type VoidResponse = ApiResponse<void>;
// { success: boolean }
type UserResponse = ApiResponse<User>;
// { success: boolean; data: User }
事件處理器型別
type EventPayload<T extends string> = T extends 'click'
? MouseEvent
: T extends 'keydown'
? KeyboardEvent
: T extends 'focus'
? FocusEvent
: Event;
function handleEvent<T extends string>(type: T, handler: (event: EventPayload<T>) => void) {
// ...
}
handleEvent('click', (e) => {
console.log(e.clientX); // e 是 MouseEvent
});
設定驗證
type RequiredKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
interface Config {
host?: string;
port?: number;
ssl?: boolean;
}
type ProductionConfig = RequiredKeys<Config, 'host' | 'ssl'>;
// host 和 ssl 變成必要
總結
條件型別是 TypeScript 型別系統中最強大的功能之一:
- 使用
extends進行型別條件判斷 - 使用
infer推論型別 - 分配行為會自動應用於聯合型別
- 可以遞迴使用來處理深層結構
- 結合映射型別可以建立複雜的型別轉換