存在型とそのエンコーディング
「型は未知だが何らかの具体型が存在する」ことを表す型。全称量化 (パラメトリック多相, ∀T) が「呼び出し側が型を選ぶ」のに対し、存在量化 (∃T) は「中身が型を知っているが外からは隠蔽されている」関係を表す。Tagged Union のパターンマッチを汎用関数化する際に必要になり、TypeScript には存在型が無いため回避テクニックを使う、という文脈で現れる (ts-discriminated-union-match)。
なぜ必要になるか
判別共用体 U = A | B | ... の各タグに対応する処理を持つ matchers を、match(item, key, matchers) という汎用関数で呼びたい。item: T extends U は共用体 全体 を表す型だが、matchers[item[key]] の引数は共用体の ある具体型 を要求する。両者が一致せず型エラーになる。
本来これは「item の実体は U のどれか1つの型 T である (その T が何かは静的には不明)」という 存在型 で表すべき状況。∃T. (item: T, matcher: T => void) のように、同じ未知の T で値と処理を束ねられれば安全に呼び出せる。
カプセル化によるエンコーディング
TypeScript の型システムに存在型は無いので、クロージャ (カプセル化) で隠蔽 して回避する (uhyo「カプセル化で我慢しよう」)。T を関数シグネチャの外に直接露出させず、getMatcher の内側に閉じ込める。
const getMatcher = <T extends U, U extends Record<K, string>, K extends Key>(
item: T, key: K, matchers: Matchers<U, K>
): (item: T) => void => (item) => matchers[item[key]];
const match = <T extends U, U extends Record<K, string>, K extends Key>(
item: T, key: K, matchers: Matchers<U, K>
) => getMatcher(item, key, matchers)(item);T が結果型に現れない形にすることで、コンパイラに具体化を強制せず型エラーを解消できる。これは存在型を「ある型を内部に閉じ込めた抽象データ」として実現する古典的手法 (存在型 ≒ 情報隠蔽・モジュール) の TypeScript 版にあたる。
一般論
- 存在型は OCaml の first-class modules、Haskell の
forallを伴うdata Showable = forall a. Show a => MkShowable a、Rust のdyn Trait/impl Traitなどで提供される。これらはいずれも「中身の型を隠して、それに対する操作だけを公開する」点で共通。 - 全称 ↔ 存在の双対性:
(∃x. P(x)) → Qは∀x. (P(x) → Q)と同型 (継続渡し/CPS によるエンコード)。上記のカプセル化も、この同型を使って存在型を全称型 (普通のジェネリック関数) に変換していると見なせる。