https://zenn.dev/qnighy/articles/462baa685c80e2

TypeScriptではデザインパターンとしてtagged unionによる直和がよく使われます。このときパターンマッチに相当する処理はswitchで行われますが、そこで直和に対する分岐が網羅的であることの保証を実行時と型検査時の両方で賢く行う方法がこれまでも模索されてきました。

今回、ヘルパー関数を導入せずにいくつかの問題を同時に解決する賢い方法を思い付いたので共有します。

コード

これだけです。

// switch (action.type) { ... default:
  throw new Error(`Unknown type: ${(action as { type: "__invalid__" }).type}`);
// .. }

以下、より詳しく説明します。

問題

TypeScriptではオブジェクトに type プロパティーを用意し、決まった文字列を入れることで直和を実現するというデザインパターンが広く使われています。このとき、パターンマッチに相当する処理はswitchで行われます。

// GetまたはPutのどちらかをあらわす型
type Action = GetAction | PutAction;
type GetAction = {
  type: "Get";
  name: string;
};
type PutAction = {
  type: "Put";
  name: string;
  value: string;
};

function act(action: Action) {
  // Getの場合とPutの場合で場合分けする
  switch (action.type) {
    case "Get":
      console.log(`get(${action.name})`);
      break;
    case "Put":
      console.log(`put(${action.name}, ${action.value})`);
      break;
  }
}

しかし、このようにswitchを使った場合、以下のような問題があります。

そこで、型検査時と実行時の両方で、分岐の網羅性をチェックする方法がこれまで模索されてきました。

素朴な実行時チェック

素朴な方法としては以下のようなコードが考えられます。

  // Getの場合とPutの場合で場合分けする
  switch (action.type) {
    case "Get": /* ... */
    case "Put": /* ... */
    default:
      throw new Error(`Unknown type: ${action.type}`);
  }

これには次のような問題があります。