TypeScript 速習チュートリアル

TypeScript 条件付き型

1. TypeScript における Conditional Types の理解

TypeScript の Conditional Types(条件付き型) を使用すると、JavaScript の if-else ステートメントのように、他の型に依存して決定される型を作成できます。
これは非常に強力な機能であり、高度な型変換や型レベルプログラミングを実現するための基盤となります。

1.1 主要なコンセプト

  • 型レベルのロジック: 型に対して条件チェックを実行します。
  • 型推論 (Type inference): infer キーワードを使用して、型を抽出および操作します。
  • コンポジション: 他の TypeScript 機能と組み合わせて複雑な構造を構築します。
  • ユーティリティ型: 強力な型ユーティリティを自作できます。

1.2 一般的なユースケース

  • 型安全な関数のオーバーロード
  • API レスポンスの型変換
  • 複雑な型のバリデーション
  • 再利用可能な型ユーティリティの構築
  • 高度な型推論

2. Conditional Types の基本構文

Conditional Types は T extends U ? X : Y という形式をとります。これは次のような意味を持ちます:
「もし型 T が型 U に割り当て可能(継承している)なら型 X を使用し、そうでなければ型 Y を使用する」。

2.1 基本的なコード例

コード例

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

// 使用例
type Result1 = IsString<string>;  // true
type Result2 = IsString<number>;  // false
type Result3 = IsString<"hello">; // true (リテラル型はベースとなる型を継承するため)

// 変数に対しても使用可能
let a: IsString<string>; // a の型は 'true'
let b: IsString<number>; // b の型は 'false'

3. Union 型と Conditional Types

3.1 ディストリビューティブ(分配的)な Conditional Types

Conditional Types が Union 型(共用体型)に対して使用されると、自動的に Union の各メンバーに適用されます。これを Distributive Conditional Types と呼びます。

コード例

type ToArray<T> = T extends any ? T[] : never;

// Union 型に使用すると、各メンバーに適用される
type StringOrNumberArray = ToArray<string | number>;
// これは内部的に ToArray<string> | ToArray<number> と展開される
// 結果として string[] | number[] となる

// Union 型から特定の型を抽出することも可能
type ExtractString<T> = T extends string ? T : never;
type StringsOnly = ExtractString<string | number | boolean | "hello">;
// 結果: string | "hello"

4. infer による型推論

4.1 複雑な構造からの型抽出

infer キーワードを使用すると、Conditional Types の条件部分で型変数を宣言し、真のブランチ(? の後)でその型を使用できます。

コード例

// 関数型の戻り値の型を抽出する
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// 例
function greet() { return "Hello, world!"; }
function getNumber() { return 42; }

type GreetReturnType = ReturnType<typeof greet>;   // string
type NumberReturnType = ReturnType<typeof getNumber>; // number

// 配列から要素の型を抽出する
type ElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayElement = ElementType<number[]>; // number
type StringArrayElement = ElementType<string[]>; // string

5. 組み込みの Conditional Types

TypeScript の標準ライブラリには、いくつかの組み込み Conditional Types が含まれています。

5.1 標準ライブラリのユーティリティ

コード例

// Extract<T, U> - T の中から U に割り当て可能な型を抽出
type OnlyStrings = Extract<string | number | boolean, string>; // string

// Exclude<T, U> - T の中から U に割り当て可能な型を除外
type NoStrings = Exclude<string | number | boolean, string>; // number | boolean

// NonNullable<T> - T から null と undefined を削除
type NotNull = NonNullable<string | null | undefined>; // string

// Parameters<T> - 関数型からパラメータ(引数)の型をタプルとして抽出
type Params = Parameters<(a: string, b: number) => void>; // [string, number]

// ReturnType<T> - 関数型から戻り値の型を抽出
type Return = ReturnType<() => string>; // string

6. 高度なパターンとテクニック

6.1 再帰的な Conditional Types

Conditional Types を再帰的に使用することで、複雑な型変換を作成できます。

コード例

// Promise 型を深くアンラップする
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;

// 例
type A = UnwrapPromise<Promise<string>>;       // string
type B = UnwrapPromise<Promise<Promise<number>>>; // number
type C = UnwrapPromise<boolean>;               // boolean

6.2 型レベルの If-Else チェーン

複数の条件を連結して、複雑な型ロジックを構築できます。

コード例

type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

// 使用例
type T0 = TypeName<string>;     // "string"
type T1 = TypeName<42>;         // "number"
type T2 = TypeName<true>;       // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<Date[]>;     // "object"

6.3 ジェネリックユーティリティでの応用

コード例

// 入力型に基づいて異なる戻り値の型を返す関数
function processValue<T>(value: T): T extends string
  ? string
  : T extends number
  ? number
  : T extends boolean
  ? boolean
  : never {

  if (typeof value === "string") {
    return value.toUpperCase() as any; // 実装上の制限により型アサーションが必要
  } else if (typeof value === "number") {
    return (value * 2) as any;
  } else if (typeof value === "boolean") {
    return (!value) as any;
  } else {
    throw new Error("サポートされていない型です");
  }
}

// 使用例
const stringResult = processValue("hello"); // 戻り値は "HELLO" (型は string)
const numberResult = processValue(10);      // 戻り値は 20 (型は number)
const boolResult = processValue(true);      // 戻り値は false (型は boolean)

7. ベストプラクティス

7.1 推奨事項 (Do)

  • 複雑な型変換が必要な場合に Conditional Types を使用する。
  • 型の抽出には infer と組み合わせて使用する。
  • 再利用可能な型ユーティリティを作成する。
  • 複雑な Conditional Types にはドキュメント(コメント)を残す。
  • 型定義の際のエッジケースをテストする。

7.2 非推奨事項 (Don't)

  • 単純な型で済む場合に、複雑な Conditional Types を過用しない。
  • 理解が困難なほど深くネストされた Conditional Types を作成しない。
  • 非常に複雑な型によるコンパイルパフォーマンスへの影響を無視しない。
  • 実行時のロジックのために Conditional Types を使用しない(型チェックのためだけに使う)。

8. パフォーマンスに関する考慮事項

  • 深くネストされた Conditional Types は、コンパイル時間を増大させる可能性があります。
  • 中間結果には型エイリアスを使用することを検討してください。
  • TypeScript の再帰深度の制限に注意してください。