🎯

【type-challengesに挑戦!】Exclude の解答と解説

に公開

はじめに

この記事は、type-challenges の問題を解説するシリーズです。今回は Exclude の問題に挑戦します。

問題

TypeScript の組み込みユーティリティ型 Exclude<T, U> を使用せずに実装します。

Exclude<T, U> は、ユニオン型 T から型 U に割り当て可能な型を除外して新しい型を構築します。

例:

type Result = MyExclude<'a' | 'b' | 'c', 'a'>
// 期待される型: 'b' | 'c'

type Result2 = MyExclude<'a' | 'b' | 'c', 'a' | 'b'>
// 期待される型: 'c'

type Result3 = MyExclude<string | number | (() => void), Function>
// 期待される型: string | number

解答

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

解説

ステップ 1: 型パラメータの定義

type MyExclude<T, U>

2 つの型パラメータを定義しています:

  • T: 元となるユニオン型(例: 'a' | 'b' | 'c')
  • U: 除外したい型(例: 'a')

ステップ 2: 条件型 T extends U ? never : T

T extends U ? never : T

この構文は条件型と呼ばれ、以下の動作をします:

  • T extends U: 型 T が型 U に割り当て可能かチェック
  • ? never : T: 条件が真なら never、偽なら T を返す

ステップ 3: 分配的条件型の理解

ここで重要なのは、分配的条件型(Distributive Conditional Types) の動作です。

条件型がユニオン型に適用されると、TypeScript は各メンバーに対して条件をチェックします。

具体例で見てみましょう:

type Result = MyExclude<'a' | 'b' | 'c', 'a'>;

この場合、以下のように展開されます:

  1. T は 'a' | 'b' | 'c' のユニオン型
  2. TypeScript は各メンバーに対して条件型を適用:
    • 'a' extends 'a' ? never : 'a' → never('a' は除外される)
    • 'b' extends 'a' ? never : 'b' → 'b'('b' は残る)
    • 'c' extends 'a' ? never : 'c' → 'c'('c' は残る)
  3. 結果を結合: never | 'b' | 'c'
  4. never はユニオン型から消える: 'b' | 'c'

複数の型を除外する場合:

type Result = MyExclude<'a' | 'b' | 'c', 'a' | 'b'>;

展開プロセス:

  1. T は 'a' | 'b' | 'c'、U は 'a' | 'b'
  2. 各メンバーに対して条件をチェック:
    • 'a' extends 'a' | 'b' ? never : 'a' → never('a' は除外)
    • 'b' extends 'a' | 'b' ? never : 'b' → never('b' は除外)
    • 'c' extends 'a' | 'b' ? never : 'c' → 'c'('c' は残る)
  3. 結果: 'c'

ステップ 4: 型階層での動作

Exclude は型の階層関係でも機能します:

type Result = MyExclude<string | number | (() => void), Function>;

展開プロセス:

  1. Function 型は関数型を表す
  2. 各メンバーに対してチェック:
    • string extends Function ? never : string → string(関数ではない)
    • number extends Function ? never : number → number(関数ではない)
    • (() => void) extends Function ? never : (() => void) → never(関数型は除外)
  3. 結果: string | number

ステップ 5: never 型の役割

? never : T

never 型は、TypeScript において「決して発生しない値の型」を表します。ユニオン型内の never は自動的に除外されるため、フィルタリングに最適です:

type Example = string | never | number;
// 結果: string | number(never は消える)

型安全性の例:

// ✅ OK: 'a' を除外
type Valid1 = MyExclude<'a' | 'b' | 'c', 'a'>;
// 結果: 'b' | 'c'

// ✅ OK: 複数の値を除外
type Valid2 = MyExclude<'a' | 'b' | 'c', 'a' | 'b'>;
// 結果: 'c'

// ✅ OK: 全て除外されると never
type Valid3 = MyExclude<'a', 'a'>;
// 結果: never

// ✅ OK: 何も除外されない場合は元の型
type Valid4 = MyExclude<'a' | 'b', 'c'>;
// 結果: 'a' | 'b'

まとめ

この記事では、type-challenges の Exclude 問題を通じて、以下の TypeScript の重要な概念を学びました:

  • 条件型: T extends U ? X : Y の構文で型レベルの条件分岐を実現
  • 分配的条件型: ユニオン型の各メンバーに条件型を自動的に適用する仕組み
  • never 型: ユニオン型から自動的に除外される特殊な型で、フィルタリングに活用
  • 型の割り当て可能性: extends を使った型の互換性チェック
  • ユニオン型の操作: 型レベルでユニオンから特定の型を除外する方法

参考リンク

Discussion