🎯
【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'>;
この場合、以下のように展開されます:
-
Tは'a' | 'b' | 'c'のユニオン型 - TypeScript は各メンバーに対して条件型を適用:
-
'a' extends 'a' ? never : 'a'→never('a' は除外される) -
'b' extends 'a' ? never : 'b'→'b'('b' は残る) -
'c' extends 'a' ? never : 'c'→'c'('c' は残る)
-
- 結果を結合:
never | 'b' | 'c' -
neverはユニオン型から消える:'b' | 'c'
複数の型を除外する場合:
type Result = MyExclude<'a' | 'b' | 'c', 'a' | 'b'>;
展開プロセス:
-
Tは'a' | 'b' | 'c'、Uは'a' | 'b' - 各メンバーに対して条件をチェック:
-
'a' extends 'a' | 'b' ? never : 'a'→never('a' は除外) -
'b' extends 'a' | 'b' ? never : 'b'→never('b' は除外) -
'c' extends 'a' | 'b' ? never : 'c'→'c'('c' は残る)
-
- 結果:
'c'
ステップ 4: 型階層での動作
Exclude は型の階層関係でも機能します:
type Result = MyExclude<string | number | (() => void), Function>;
展開プロセス:
-
Function型は関数型を表す - 各メンバーに対してチェック:
-
string extends Function ? never : string→string(関数ではない) -
number extends Function ? never : number→number(関数ではない) -
(() => void) extends Function ? never : (() => void)→never(関数型は除外)
-
- 結果:
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を使った型の互換性チェック - ユニオン型の操作: 型レベルでユニオンから特定の型を除外する方法
参考リンク
- https://github.com/type-challenges/type-challenges
- https://github.com/type-challenges/type-challenges/blob/main/questions/00043-easy-exclude/README.md
- https://www.typescriptlang.org/docs/handbook/utility-types.html
- https://www.typescriptlang.org/docs/handbook/2/conditional-types.html
- https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
Discussion