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 推論型別
  • 分配行為會自動應用於聯合型別
  • 可以遞迴使用來處理深層結構
  • 結合映射型別可以建立複雜的型別轉換