はじめに
この投稿は、TypeScript学習者が書いています。条件型の読み方と、ユーティリティ型のExcludeがユニオン型の要素ごとにどう展開されるかを、自身の理解のために整理します。
1. 条件型とは
条件型は次のように書かれます。
T extends U ? X : Y
-
T… 判定する型 -
U… 割り当て先として比べる型 -
X…TがUに割り当て可能なときの型 -
Y… 割り当てできないときの型
TがUに割り当て可能ならX、そうでなければYになります。
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">;
// true
type B = IsString<number>;
// false
この例ではUがstring、Xがtrue、Yがfalseです。
2. ユニオン型では要素ごとに分配される
Tがユニオン型のとき、条件型は各要素に分配されます。
type ToName<T> = T extends string ? "str" : "other";
type Names = ToName<string | number>;
// "str" | "other"
ToName<string | number>は、次と同じ意味です。
type Names = ToName<string> | ToName<number>;
// "str" | "other"
string | number全体がstringに割り当て可能かを一度に見るのではありません。全体判定ならnumberを含むので結果は"other"だけになります。実際は要素ごとに分岐し、結果をユニオン型でつなぎます。
3. Excludeの定義
TypeScript組み込みのExcludeは、次の条件型です。
type Exclude<T, U> = T extends U ? never : T;
Statusから"archived"を除く例です。
type Status = "draft" | "published" | "archived";
type Editable = Exclude<Status, "archived">;
// "draft" | "published"
第2節と同じく、Exclude<Status, "archived">は要素ごとに分かれます。
type Editable =
| Exclude<"draft", "archived">
| Exclude<"published", "archived">
| Exclude<"archived", "archived">;
各要素で、その要素が"archived"に割り当て可能かを見ます。
// "draft" extends "archived" ? never : "draft"
// → "draft"
// "published" extends "archived" ? never : "published"
// → "published"
// "archived" extends "archived" ? never : "archived"
// → never
neverはユニオン型から消えるので、残りは"draft" | "published"です。
const ok: Editable = "draft";
const ng: Editable = "archived"; // 型エラー
// Type '"archived"' is not assignable to type 'Editable'.
ExtractはExcludeと逆で、Uに割り当て可能な要素だけを残します。
type Extract<T, U> = T extends U ? T : never;
type Closed = Extract<Status, "archived" | "published">;
// "published" | "archived"
まとめ
- 条件型
T extends U ? X : Yは、TがUに割り当て可能かで型を分岐します。 -
Tがユニオン型のとき、条件型は要素ごとに分配されます。全体を一度に判定するのではありません。 -
Exclude<T, U>は各要素をT extends U ? never : Tで評価し、neverになった要素を除きます。