🧩

TypeScript の条件型と分配法則、あるいはユニオン型の写像

2024/03/27に公開

TypeScript の Extract について調べていたら、自分がユニオン型の分配法則について何も理解していなかったことに気づいたので、記事にまとめておく。

Extract の基本的な使い方

// https://typescriptbook.jp/reference/type-reuse/utility-types/extract
type Grade = "A" | "B" | "C" | "D" | "E";
type FailGrade = Extract<Grade, "D" | "E">; //=> "D" | "E"

これは単に ("A" | "B" | "C" | "D" | "E") & ("D" | "E") のインターセクションを取ってるだけなのでは? と今まで考えていたが、全然違った。

Extract 型の TypeScript 上の定義はこうなっている。

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

インターセクション(&) なんて出てこない。T と U のサブタイプ関係を見て Tnever を返している。これを踏まえて先の例を見ると、自分の理解だと、("A" | "B" | "C" | "D" | "E""D" | "E" のサブタイプではないので、これは never になるはずでは? と思って挙動を確認した。

// 特別扱いされてないかの確認
type Extract1<T, U> = T extends U ? T : never;
// 自分の理解
type Extract2<T, U> = T & U;
type _original = Extract<1 | 2 | 3, 2 | 3 | 4> //=> 2 | 3
type _1 = Extract1<1 | 2 | 3, 2 | 3 | 4> //=> 2 | 3
type _2 = Extract2<1 | 2 | 3, 2 | 3 | 4> //=> 2 | 3

これは一体 インターセクションとどう違うんだ? と ChatGPT に聞いてみた。どうやら条件型の理解が全然違っていて、ユニオン型の分配法則というのがあるらしい。

ユニオン型の分配法則

条件型 T extends U ? ... : ... で、 T がユニオン型のとき、分配法則が適用される。

type T = "A" | "B" | "C";
type R = T extends "B" | "C" ? T : never;
// R は次のように展開される
type R_ =
  | ("A" extends "B" | "C" ? "A" : never)
  | ("B" extends "B" | "C" ? "B" : never)
  | ("C" extends "B" | "C" ? "C" : never)
  ; //=> "B" | "C"

自分は誤解の元は、条件型は単にサブタイプ条件を満たすかどうかをチェックしてから分岐する三項演算子だと思っていたが、実際には T extends ... で T がユニオン型の場合、T の 集合に対する写像だったという話。

写像 - Wikipedia

確かに集合に対する射として考えるとスッと理解できるが、分配法則を知らずに三項演算子だと思い込んでると絶対に辿り着けない話だった。

ChatGPT はこの誤解を解いてくれるところまで進化しててすごい。

https://chat.openai.com/share/acb96919-191b-4898-8664-dad3c501c9a7

Discussion