tags : TypeScript

TypeScript 型制約テクニック: 「最低限含む型」を保証する _proof

背景

  • 通常のジェネリクス制約 `X extends AB` は
    • X が AB に代入可能な型(= AB 以下の型)しか許さない
    • AB より広い型(例: AB | C)は許されない
  • 「X が AB を最低限含む(= AB ⊆ X)」という条件は直接は書けない

解決策

  • ダミー引数 `_proof` を追加して、型条件を満たさない場合にコンパイルエラーにする

  • 型条件:

    _proof: AB extends X ? [] : ["X must include A & B"]
    • `AB extends X` が true → 型は `[]` になり、引数を渡さなくてよい
    • false → 型は要素1つの配列になり、呼び出し時に渡せないため型エラー

実装例

type A  = { type: 'A'; n: number };
type B  = { type: 'B'; s: string };
type AB = A | B;
 
function handle<X>(
  value: X,
  ..._proof: AB extends X ? [] : ["X must include A & B"]
) {
  console.log(value);
}
 
// OK
handle<AB>({ type: 'A', n: 1 });
handle<AB | { type: 'C'; flag: boolean }>({ type: 'C', flag: true });
 
// NG: AB を含まない
handle<{ type: 'C'; flag: boolean }>({ type: 'C', flag: true });
// → 型エラー: X must include A & B

バリエーション

  • 可変長引数 `…_proof` の代わりに通常引数でも可能
    function handle<X>(
      value: X,
      _proof?: AB extends X ? undefined : ["X must include A & B"]
    ) { /* ... */ }
  • 可変長にする理由:
    • `[]` で「何も渡さない」ことを自然に表現できる
    • 条件が false のとき、即配列型不一致でエラーにできる

利用シーン

  • 「X は最低限このバリアント群を含んでいる」ことを保証したいとき
  • ts-pattern 等のパターンマッチで必須ケースを型安全に処理したいとき
  • Effect/Schema 等で decode 後の型を保証する用途