JavaScriptにRustの良いところをちょい足ししてみた: match編

Rust愛好家の私が、JavaScriptにもあったら良いなと思うRustの機能をちょい足ししていくシリーズ（予定）です。
今回は、JavaScriptの `switch` 文の上位互換とも言うべきRustの `match` 式風の `match` 関数をちょい足ししてみます。

# 対象読者

JavaScript初心者でも読めるように書いてみましたので、JavaScriptに少しでも馴染みのある方すべてを対象にしています。

TypeScriptをお使いの方には更に追加ポイントとして、今回紹介する `match` 関数がいかに型安全性を実現しプログラムのエラーをいち早く検知できるようになっているか、その裏側を解説いたします。
TypeScriptに詳しくない方は読み飛ばせるようになっているので、ご心配なく。

また、Rustに関する知識は一切必要ありませんので、その点もご心配なく。

# JavaScriptの `switch` 文、使いづらくないですか？

まずは例として「サブスクリプションサービスの契約ステータス」をプログラム上で表現することを考えてみます。
ここでは、契約ステータスには以下の3種類があるとします。

- 契約中
- 過去に契約していたが、現在は解約済み
- 一度も契約したことがない

このステータスを元に、（例えばWebアプリのフロントエンドなどに表示する）日本語のメッセージを表示することを考えます。
さらに、それぞれのステータスごとに追加で以下の情報も表示したいとしましょう。

- 契約中
  - 次回契約更新日時
  - 契約の自動更新が有効か
- 過去に契約していたが、現在は解約済み
  - 解約日時
- 一度も契約したことがない

（「一度も契約したことがない」ステータスの場合は追加の情報はありません。）

では、JavaScriptコードを書いていきましょう。
はじめに、それぞれのステータスを表すオブジェクトを変数として定義してみます。

```js
// 「契約中」ステータス。
const subscribed = {
  // ステータスを区別するための項目。
  kind: "subscribed",
  // 次回契約更新日時。
  nextRenewalDate: new Date(),
  // 契約の自動更新が有効か。
  autoRenewalEnabled: false,
};

// 「過去に契約していたが、現在は解約済み」ステータス。
const subscriptionStopped = {
  kind: "subscriptionStopped",
  // 解約日時。
  stopTime: new Date(),
};

// 「一度も契約したことがない」ステータス。
const neverSubscribed = {
  kind: "neverSubscribed",
};
```

ここでは、それぞれのオブジェクトの `kind` プロパティを基準にどのステータスであるかを区別するという想定をしています。
こういった目的に `kind` ではなく `type` という名前のプロパティを使う方もいますが、JavaScriptの世界における「型」と名前が衝突してしまっているので私は避けています。
（TypeScriptを使っていないからといって型という概念が存在しないわけではないですよね。`typeof subscribed === "object"` ですから。）

続いて、これらのステータスを日本語メッセージに変換する部分の処理を `switch` 文を使って書いてみます。

```js
// 契約ステータスの日本語メッセージを返す。
const describeStatus = (status) => {
  switch (status.kind) {
    case "subscribed":
      return `サブスクリプション契約中です。${toDateString(status.nextRenewalDate)}に${
        status.autoRenewalEnabled ? "自動更新されます。" : "無効となります。"
      }`;
    case "subscriptionStopped":
      return `サブスクリプション契約は${toDateString(status.stopTime)}に解約されました。`;
    case "neverSubscribed":
      return "サブスクリプション未契約です。";
  }
};

// Dateオブジェクトを `YYYY/MM/DD` 形式でフォーマットする。
const toDateString = (date) =>
  date.toLocaleDateString("ja-JP", { dateStyle: "medium" });
```

:::note info
それぞれの `case` 節が `break` で終わらないことに違和感を感じられる方もいるかもしれません。
 `case` 節が `break` で終わることが一般的なのは事実ですが、 `break` 以外の方法で `switch` 文から抜け出すこともよくあります。
この例で使っている `return` 以外にも、 `throw` で例外を発生させる方法などもあります（後ほど登場します）。
`return` の次の行に `break` を書いても動かなくなることはありませんが、その行に処理が到達することはないので無駄にキーボード叩いていることになります。
:::

そして最後に、 `describeStatus` を使ってメッセージをコンソールに出力してみます。

```js
console.log(describeStatus(subscribed));
// → サブスクリプション契約中です。2023/02/09に無効となります。

console.log(describeStatus(subscriptionStopped));
// → サブスクリプション契約は2023/02/09に解約されました。

console.log(describeStatus(neverSubscribed));
// → サブスクリプション未契約です。
```

ここまで読んで、この `switch` 文を使った例のどこが悪いのかとお感じの方もいらっしゃると思います。
正直なところ、このようなJavaScriptのコードは普通に見かけますし、私自身この `switch` 文を見て「悪いコード」とは一切思いません。
それでも、Rustの `match` 式の良さを一度味わってしまった私には、JavaScriptの `switch` 文には「これじゃない感」を感じる点が3つほどあるのです。

## 1. `switch`「文」 vs. `match`「式」

なんとなく読んでいると気づきにくい話ではあるのですが、ここまでJavaScriptの `switch` 「文」とRustの `match` 「式」と言葉を使い分けしていたことに気づいた方もいらっしゃると思います。
私は細かい定義の話は好きじゃないので、次の例を見ていただきたいです。

:::note alert
この例は正しいJavaScriptでないので、動きません。

```js
const message = switch (status.kind) {
  case "subscribed":
    `サブスクリプション契約中です。${toDateString(status.nextRenewalDate)}に${
      status.autoRenewalEnabled ? "自動更新されます。" : "無効となります。"
    }`;
  case "subscriptionStopped":
    `サブスクリプション契約は${toDateString(status.stopTime)}に解約されました。`;
  case "neverSubscribed":
    "サブスクリプション未契約です。";
}
```
:::

これは、JavaScriptの `switch` が「文」であるにも関わらず「式」として使おうとしているため、正しいJavaScriptコードではありません。
式は、プログラム実行時にその計算結果の値に置き換わる（「式が評価される」とも言いますが）ものです。
例えば、 `2 + 2` は実行時に計算結果の `4` に置き換わるので式です。
`parseInt("0xff", 16)` のような関数の呼び出しも式です（ここでは `255` に置き換わりますね）。
逆に、 `switch` や `for` のような構文は文であるため、それ自体に計算結果の値というものは存在しません。
このため、上の例のように「変数に値を代入する」場面での値としては式を使わなければならず、文を使った場合は文法エラーとなってしまいます。

どの機能が式か文かというのはプログラミング言語によって意見が分かれる部分ではあります。
一昔前はRubyの `case` （JavaScriptの `switch` と役割は同じ）や `if` が文ではなく式であることでRubyの表現性の高さが売りにもなっていましたが、KotlinやRustなどの比較的新しめの言語は文よりも式を好む傾向が強くなってきていると感じます。
今回の記事の元となったRustの `match` も式になっています。
次の例を見ていただければ雰囲気は掴めるかと思います。

```rust
let message = match status {
    Status::Subscribed => "サブスクリプション契約中です。",
    Status::SubscriptionStopped => "サブスクリプション契約は解約されました。",
    Status::NeverSubscribed => "サブスクリプション未契約です。",
};
```

`switch` 式のようなものをJavaScriptで実現したい場合、 `switch` 文を関数式に包み込んで、その関数式を直ちに実行するという方法があります。

```js
const message = (() => {
  switch (status.kind) {
    case "subscribed":
      return `サブスクリプション契約中です。${toDateString(status.nextRenewalDate)}に${
        status.autoRenewalEnabled ? "自動更新されます。" : "無効となります。"
      }`;
    case "subscriptionStopped":
      return `サブスクリプション契約は${toDateString(status.stopTime)}に解約されました。`;
    case "neverSubscribed":
      return "サブスクリプション未契約です。";
  }
})();
```

今回の例に限らず頻繁に使われることの多いパターンなので、「即時実行関数」という名前まであります。
ある程度JavaScriptに慣れてしまえば戸惑うこともないですが、私が初心者だった頃は謎の暗号を読み解いているような気分になった思い出があります。
こんな初心者泣かせの機能を使うのが定石な世の中にはしたくないじゃないですよね？
これが、Rust風 `match` をJavaScriptにちょい足ししたいと思った1つめの理由です。

## 2. 全パターンを網羅していない場合でも気づきにくい

仮に開発を進めていく過程で契約ステータスの種類が3種類から4種類になったとします。
例えば、「契約不可能（`kind = "subscriptionUnavailable"`）」を表すステータスを追加することを考えましょう。

```js
const subscriptionUnavailable = {
  kind: "subscriptionUnavailable",
};
```

ここで、本来であれば契約ステータスを扱っている部分のコードすべてを「契約不可能」ステータスの場合に対応できるよう書き直さなければならないですが、プログラマの不注意で修正し忘れてしまう場合もあるでしょう。
一例として、先程使った `describeStatus` を「契約不可能」ステータスに対応せずに「契約不可能」ステータスを引数として渡した場合を考えてみます。

```js
const describeStatus = (status) => {
  switch (status.kind) {
    case "subscribed":
      return "サブスクリプション契約中です。";
    case "subscriptionStopped":
      return "サブスクリプション契約は解約されました。";
    case "neverSubscribed":
      return "サブスクリプション未契約です。";
    // subscriptionUnavailableの場合を追加していない！
  }
};
```

```js
console.log(describeStatus(subscriptionUnavailable));
// → undefined
```

ご覧の通り、`switch` 文で `kind` が `subscriptionUnavailable` の場合を扱っていないため、いずれの `case` 節も実行されずに関数の終端に到達することになります。
この場合、 `return` 文で具体的な値を返さずに関数の終端に到達したため、関数の戻り値は `undefined` となります。
関数の戻り値を後続の処理でどう使っているか次第ではありますが、例えば戻り値をそのままフロントエンドの画面に埋め込んでる場合ですと、 `undefined` が空の文字列に変換されるためエラーにならない、エラーにならないため修正し忘れたことにも気づけないといった結果になるかもしれません。

このような場合にエラーが発生するよう、 `default` 節で `kind` の値が想定外だった場合に例外を発生させるというのも一つの手です。

```js
const describeStatus = (status) => {
  switch (status.kind) {
    case "subscribed":
      return "サブスクリプション契約中です。";
    case "subscriptionStopped":
      return "サブスクリプション契約は解約されました。";
    case "neverSubscribed":
      return "サブスクリプション未契約です。";
    default:
      // subscriptionUnavailableの場合に実行される。
      throw new Error(`未対応の契約ステータス${status.kind}が指定されました。`);
  }
};
```

この `default` 節は `status.kind` が3つの `case` 節のいずれにも当てはまらない場合のみ実行されるため、 `subscriptionUnavailable` の `case` 節を追加せずに「契約不可能」の値で `describeStatus` を実行した場合は例外が発生することとなります。
この方法を使えば全パターンを網羅できない場合の検知がしやすくなるのは事実ですが、すべての `switch` 文にこの例外処理を追加するのは手間ですし、追加すること自体を忘れてしまう可能性さえあります。

このような、不注意による不具合の見逃しを少なくしたいというのが、Rust風 `match` をJavaScriptに追加したいと思った2つ目の理由です。

## 3. 記法がヘビー

Rustが使ってて気持ちの良い言語である理由の一つに、「タイプ量が少ない」という点があると思います。
型のタイプではなく、キーボードのタイプです。
その理由の一つとして、頻繁に使われるキーワードの多くが2・3文字のみに抑えられているという点が挙げられます。
以下、いくつか例を挙げてみます。

<table>
  <thead>
    <tr>
      <th>JavaScript</th>
      <th>Rust</th>
    </tr>
  </thead>
  <tr>
    <td>
      <code>function</code>
    </td>
    <td>
      <code>fn</code>
    </td>
  </tr>
  <tr>
    <td>
      <code>const</code>
    </td>
    <td>
      <code>let</code>
    </td>
  </tr>
  <tr>
    <td>
      <code>import</code>
    </td>
    <td>
      <code>use</code>
    </td>
  </tr>
  <tr>
    <td>
      <code>export</code>
    </td>
    <td>
      <code>pub</code>
    </td>
  </tr>
</table>

たかがキーストローク数回少ないだけともいえますが、塵も積もれば山となるというものです。

また、単なるキーワードの文字数という話だけではありません。
ここで、JavaScriptの `switch` 文とRustの `match` 式を視覚的に比較してみます。

<table>
  <thead>
    <tr>
      <th>JavaScript</th>
      <th>Rust</th>
    </tr>
  </thead>
  </thead>
  <tr>
    <td>
      <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305052/428d2215-f86f-559c-f4b0-3840c964fa50.png">
    </td>
    <td>
      <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305052/9d743eee-ae4d-635d-54aa-eef4aee46f93.png">
    </td>
  </tr>
</table>

それぞれの画像で赤でハイライトした部分は、それぞれの処理において「意味のある」部分で、それ以外の部分は言語の記法上書かざるを得ない部分です。
ご覧の通り、Rustの `match` 式のほうが必要な情報を無駄なくコンパクトに表現できていますよね。

JavaScriptでもこのコンパクトさを実現したいというのが、Rust風 `match` をJavaScriptに追加したいと思った3つ目の理由です。

# Rust風 `match` のご紹介

では、これまで使ってきた契約ステータスの例をRust風 `match` 関数を使って書き直してみましょう。
（実装方法については「`match` 関数の実装解説（JavaScript編）」にて紹介します。）

```js
const message = match(status, "kind")({
  subscribed: ({ nextRenewalDate, autoRenewalEnabled }) =>
    `サブスクリプション契約中です。${toDateString(nextRenewalDate)}に${
      autoRenewalEnabled ? "自動更新されます。" : "無効となります。"
    }`,
  subscriptionStopped: ({ stopTime }) =>
    `サブスクリプション契約は${toDateString(stopTime)}に解約されました。`,
  neverSubscribed: () => "サブスクリプション未契約です。",
});

// Dateオブジェクトを `YYYY/MM/DD` 形式でフォーマットする。
const toDateString = (date) =>
  date.toLocaleDateString("ja-JP", { dateStyle: "medium" });
```

ご覧の通り、 `match` 関数の使い方には少々クセがあります。
（後ほどご紹介する「`match` 関数の実装解説（TypeScript編）」をお読みになれば、理由がお分かりになるはずです。）

`match` 関数を直接呼び出している `match(status, "kind")` 部分では、第一引数として契約ステータスオブジェクトを渡しています。
第二引数の `"kind"` は、第一引数として渡した契約ステータスオブジェクト内でステータスの区別をするために使っているプロパティの名前です。
例えばステータスの区別に `"type"` というプロパティ名を使いたい方は、 `match(status, "type")` と書くことになります。

さて、 `match(status, "kind")` の直後に丸括弧 `(` が続いていますが、これは `match(status, "kind")` の戻り値が関数であり、その戻り値の関数を即座に呼び出しているためです。
この2度目の関数呼び出しでは、唯一の引数としてオブジェクトを渡しています。
このオブジェクトは、 `status.kind` のとり得る値それぞれについてプロパティが定義されています。
ここでは、 `subscribed`, `subscriptionStopped`, `neverSubscribed` の3つのプロパティを持つオブジェクトを渡していますね。
それぞれのプロパティの値として、ステータスがその値だった場合に実行される関数を指定しています。
この関数の引数には、 `match(status, "kind")` の第一引数である `status` がそのまま渡されます。
例えば `subscribed` に指定された関数では、引数の `status` から「分割代入」を使って `nextRenewalDate` と `autoRenewalEnabled` を取り出し、それらを元にメッセージを組み立てていますね。
`subscriptionStopped` でもこのステータス固有の `stopTime` を分割代入で取り出しています。
`neverSubscribed` ではステータス固有のプロパティがなく引数を受け取る意味がないので、引数を取らない関数を指定しています。

この `match` は関数を使って実現しているため、上の例のように関数呼び出しの結果をそのまま変数に代入でき、文ではなく式として使われていることがお分かりと思います。

また、全パターンを網羅できていなかった場合についても考えてみましょう。

```js
const describeStatus = (status) => {
  return match(status, "kind")({
    subscribed: () => "サブスクリプション契約中です。",
    subscriptionStopped: () => "サブスクリプション契約は解約されました。",
    neverSubscribed: () => "サブスクリプション未契約です。",
  });
};

const subscriptionUnavailable = {
  kind: "subscriptionUnavailable",
};

console.log(describeStatus(subscriptionUnavailable));
```

このコードを実行すると、以下の例外が発生します。

```
PanicError: match branch missing for kind = subscriptionUnavailable
```

例外が発生することで、全パターンが網羅できていなかった場合や、タイポしてしまった場合にいち早く間違いに気づくことができるはずです。

さらに、このJavaScriptの `switch` 文とRust風 `match` 関数で記法を比べてみます。

<table>
  <thead>
    <tr>
      <th>JavaScript - switch</th>
      <th>JavaScript - match</th>
    </tr>
  </thead>
  </thead>
  <tr>
    <td>
      <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305052/428d2215-f86f-559c-f4b0-3840c964fa50.png">
    </td>
    <td>
      <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305052/c5a66567-604a-330d-c20b-d095dd340d6a.png">
    </td>
  </tr>
</table>

ご覧の通り、 `case` や `return` キーワードが省略できることで、比較的無駄が少なめでRustに近い記法になっています。

「文ではなく式である」「網羅性を担保できる」「無駄の少ない記法である」という3つの要件が満たされており、Rust愛好家の私でも気持ちよく使える `match` 関数になっています。

# Rust風 `match` の上級者向けの使い方

## タプルを使う

契約ステータスをオブジェクトではなくタプルとして表現したい場合にも、今回のRust風 `match` を使うことができます。
タプルはJavaScriptでは配列を使って表されることが一般的で、配列の何番目の要素にどの情報を格納するかをあらかじめ決めておく必要があります。

```js
// 「契約中」ステータス。
const subscribed = [
  // 0要素目: ステータスを区別するための項目。
  "subscribed",
  // 1要素目: 次回契約更新日時。
  new Date(),
  // 2要素目: 契約の自動更新が有効か。
  false,
];

// 「過去に契約していたが、現在は解約済み」ステータス。
const subscriptionStopped = [
  "subscriptionStopped",
  // 1要素目: 解約日時。
  new Date(),
];

// 「一度も契約したことがない」ステータス。
const neverSubscribed = ["neverSubscribed"];
```

ここでは、配列の0番目の要素にどのステータスであるかを表す文字列を、それ以降の要素にそれぞれのステータス固有の情報を格納するようにしています。
この場合、 `match(status, "kind")` と書く代わりに `match(status, 0)` と書くことで、配列の0番目の要素がステータスを区別するための項目であることを指定できます。

```js
const describeStatus = (status) => {
  return match(status, 0)({
    subscribed: ([_kind, nextRenewalDate, autoRenewalEnabled]) =>
      `サブスクリプション契約中です。${toDateString(nextRenewalDate)}に${
        autoRenewalEnabled ? "自動更新されます。" : "無効となります。"
      }`,
    subscriptionStopped: ([_kind, stopTime]) =>
      `サブスクリプション契約は${toDateString(stopTime)}に解約されました。`,
    neverSubscribed: () => "サブスクリプション未契約です。",
  });
};

// Dateオブジェクトを `YYYY/MM/DD` 形式でフォーマットする。
const toDateString = (date) =>
  date.toLocaleDateString("ja-JP", { dateStyle: "medium" });

console.log(describeStatus(subscribed));
// → サブスクリプション契約中です。2023/02/09に無効となります。

console.log(describeStatus(subscriptionStopped));
// → サブスクリプション契約は2023/02/09に解約されました。

console.log(describeStatus(neverSubscribed));
// → サブスクリプション未契約です。
```

## シンボルを使う

ステータスを区別するプロパティのキーには `kind` のような文字列のかわりに `Symbol` 型の値を使うこともできます。
また、ステータスを区別するプロパティの値としても、 `Symbol` を使うことができます。
キーと値の両方を `Symbol` とした場合の例を以下で見てみましょう。

```js
// ステータスを区別するプロパティのキー。
const kind = Symbol();

// それぞれのステータスでkindキーの値として使うSymbol値。
const subscribedKind = Symbol();
const subscriptionStoppedKind = Symbol();
const neverSubscribedKind = Symbol();

// 「契約中」ステータス。
const subscribed = {
  // ステータスを区別するための項目。
  [kind]: subscribedKind,
  // 次回契約更新日時。
  nextRenewalDate: new Date(),
  // 契約の自動更新が有効か。
  autoRenewalEnabled: false,
};

// 「過去に契約していたが、現在は解約済み」ステータス。
const subscriptionStopped = {
  [kind]: subscriptionStoppedKind,
  // 解約日時。
  stopTime: new Date(),
};

// 「一度も契約したことがない」ステータス。
const neverSubscribed = {
  [kind]: neverSubscribedKind,
};

// 契約ステータスの日本語メッセージを返す。
const describeStatus = (status) => {
  return match(status, kind)({
    [subscribedKind]: ({ nextRenewalDate, autoRenewalEnabled }) =>
      `サブスクリプション契約中です。${toDateString(nextRenewalDate)}に${
        autoRenewalEnabled ? "自動更新されます。" : "無効となります。"
      }`,
    [subscriptionStoppedKind]: ({ stopTime }) =>
      `サブスクリプション契約は${toDateString(stopTime)}に解約されました。`,
    [neverSubscribedKind]: () => "サブスクリプション未契約です。",
  });
};

// Dateオブジェクトを `YYYY/MM/DD` 形式でフォーマットする。
const toDateString = (date) =>
  date.toLocaleDateString("ja-JP", { dateStyle: "medium" });

console.log(describeStatus(subscribed));
// → サブスクリプション契約中です。2023/02/09に無効となります。

console.log(describeStatus(subscriptionStopped));
// → サブスクリプション契約は2023/02/09に解約されました。

console.log(describeStatus(neverSubscribed));
// → サブスクリプション未契約です。
```

# TypeScriptではさらに `match` 関数が強力になります

:::note info
TypeScriptに馴染みがない方も、「型」の存在によって開発効率がいかに向上できるかのデモンストレーションとなっていますので、このセクションは読んでいただきたいです。
:::

TypeScriptでは、今回例として使っている契約ステータスオブジェクトは以下のような型として定義するのが一般的です。

```ts
// 「契約中」ステータス。
interface Subscribed {
  // ステータスを区別するための項目。
  kind: "subscribed";
  // 次回契約更新日時。
  nextRenewalDate: Date;
  // 契約の自動更新が有効か。
  autoRenewalEnabled: boolean;
}

// 「過去に契約していたが、現在は解約済み」ステータス。
interface SubscriptionStopped {
  kind: "subscriptionStopped";
  // 解約日時。
  stopTime: Date;
}

// 「一度も契約したことがない」ステータス。
interface NeverSubscribed {
  kind: "neverSubscribed";
}

type Status = Subscribed | SubscriptionStopped | NeverSubscribed;
```

今回紹介しているRust風 `match` 関数は、このような型情報を活用してさらなる開発効率・安全性を実現することができます。

こちらは、以下のデモンストレーションを見ていただくのが一番分かりやすいかと思います。

![ts-match-autocomplete.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305052/9d3df95a-20f4-c64a-4d07-afe8b5e96621.gif)

注目すべき箇所は2点あります。

1点目は、 `match(status, "kind")` の第二引数の `"kind"` を自動補完している点です。
これは、第一引数である `status` のすべてのステータスで共通するプロパティのキーのみを第二引数として受け付けるように型定義しているため、今回の場合はそれに該当する唯一のプロパティ `"kind"` のみが補完候補となっています。

自動補完だけでなく、実在しないプロパティを指定した場合にエラーとなる点も注目です。

![ts-match-tag-error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305052/641b9cbb-4c99-e7a2-dddb-039c8165b55b.png)

2点目は、2回目の関数呼び出しの引数を自動補完している点です。
ここでは、1回目の関数呼び出しの第一引数である `status` の型（`Status` 型）を元に、2回目の関数呼び出しの引数に渡すべきオブジェクトの型を導き出しているため、このような補完が可能になっています。

こちらは、全ステータスを網羅しなかった場合にエラーとなる点にも注目です。

![ts-match-branch-error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305052/6dee44d8-df49-30b2-38bf-fb445925b67b.png)

お世辞にも読みやすいエラーメッセージとは言えませんが、赤でハイライトした部分を読めば `neverSubscribed` ステータスの場合の処理を書き忘れていることが分かるはずです。

このように、TypeScriptを使うことによって開発効率の向上のみならず、JavaScriptのみではいざ実行してエラーになってからでないと気づけないような間違いにも、開発中の早い段階で気づけるようになるという利点があります。
これは今回紹介しているRust風 `match` 関数に限らず全般的に言えることですので、TypeScriptに興味を持っていただけた方はこれを機に勉強してみてはいかがでしょうか？

# `match` 関数の実装解説（JavaScript編）

使い方の紹介をしたところで、実際にこのRust風 `match` 関数をいかに実装するかを解説していきます。
まずはこのセクションでJavaScriptを使って実装する方法を解説し、その次のセクションでTypeScriptを使った実装方法も紹介していきたいと思います。

JavaScript版の実装は、ご覧の通りいたって簡単です。

```js
export const match = (input, tagKey) => (branches) => {
  const tag = input[tagKey];
  const branchForTag = branches[tag];
  if (!branchForTag) {
    throw new Error(
      `match branch missing for ${String(tagKey)} = ${String(tag)}`
    );
  }
  return branchForTag(input);
};
```

使い方の紹介で「`match` 関数の戻り値である関数を即時に呼び出す」というクセのある使い方を紹介しましたが、この実装を見ると `match` が戻り値として関数を返す関数であることが見てとれると思います。
分かりにくいと思った方のために、以下のように書き直してみましょう。

```js
export const match = (input, tagKey) => {
  const executeBranch = (branches) => {
    const tag = input[tagKey];
    const branchForTag = branches[tag];
    if (!branchForTag) {
      throw new Error(
        `match branch missing for ${String(tagKey)} = ${String(tag)}`
      );
    }
    return branchForTag(input);
  };

  return executeBranch;
};
```

`executeBranch` 関数が2回目の関数呼び出しにて呼び出される関数だということが分かりやすくなったと思います。
また、1回目の関数呼び出しでは `input` と `tagKey` パラメータを受け取り、 `executeBranch` 関数を作っているだけなので、重要な処理の本体は `executeBranch` 関数に集約されていることも分かるでしょう。

では、肝心の `executeBranch` 関数の中身を見てみます。

まず1回目の関数呼び出しで受け取った `input` と `tagKey` はそれぞれ、「入力オブジェクト（例では「契約ステータスを表すオブジェクト」）」と「入力オブジェクトの種別を区別するためにつかうプロパティのキー（例では `kind`）」を表します。
「入力オブジェクトの種別を区別するために使うプロパティ」だと長ったらしいので、簡潔にタグ（`tag`）と呼んでいます。
2回目の関数呼び出しで受け取った `branches` は、それぞれのタグごとにタグの値（`subscribed` など）をキーとし、入力オブジェクトとそのタグの値が一致した場合に実行される関数を値とするオブジェクトを表します。

`const tag = input[tagKey];` では入力オブジェクトからタグの値を取り出しており、その値を元にどの関数を実行すべきかを `const branchForTag = branches[tag];` で決定しています。
もし該当する関数がなかった場合は全パターンが網羅できていないので、プログラムに誤りがあるとみなして例外を発生させています（`if` 文の中身）。
該当する関数があった場合は、その関数に入力オブジェクトを渡して実行し、その戻り値を `executeBranch` 関数そのものの戻り値として返しています。

関数が2回呼び出されることを除けばそこまで複雑な処理はないかと思います。

# `match` 関数の実装解説（TypeScript編）

:::note warn
TypeScriptにそこそこ自信のある方以外はこのセクションは読み飛ばしていただくことをおすすめします。
:::

JavaScript版の実装のみでは、先程紹介した自動補完や型安全性の担保は実現できません。
このセクションでは、先程のJavaScript実装に型情報を加えていき、自動補完・型安全性をいかに実現していくかを解説していきます。

まず、お助け型として、JavaScriptにおいてオブジェクトのキーとなり得る型を表す `ObjectKey` 型を定義しましょう。

```ts
type ObjectKey = string | symbol | number;
```

そして、もう一つのお助け型として、入力オブジェクトに必要な制約を `UnionRecord` 型として表現します。

```ts
export type UnionRecord<TTagKey extends ObjectKey> = Record<TTagKey, ObjectKey>;
```

`TTagKey` 型引数には `"kind"` のようなタグのキーを表すリテラル型が入ることになります。
試しに `TTagKey = "kind"` として型を展開していくと、以下のようになります。

```ts
type U1 = UnionRecord<"kind">;
type U2 = Record<"kind", string | symbol | number>;
type U3 = {
  kind: string | symbol | number;
}
```

つまり、タグのキーが `"kind"` である入力オブジェクトに必要な制約とは、 `kind` プロパティの値が `string | symbol | number` 型であること、と言えます。
タグの値の型に制約があるのは、関数の2回目の呼び出しの際にそれぞれのタグの値をキーとするオブジェクトを渡す必要があるためです。

さて、続けて `match` 関数の本体に型情報を追加していきます。
まずは1回目に呼び出される関数にのみ、型情報を追加してみます。

```diff_typescript
- export const match = (input, tagKey) => {
+ export const match = <
+  TUnion extends UnionRecord<TTagKey>,
+  TTagKey extends keyof TUnion
+>(
+  input: TUnion,
+  tagKey: TTagKey
+) => {
  const executeBranch = (branches) => {
    const tag = input[tagKey];
    const branchForTag = branches[tag];
    if (!branchForTag) {
      throw new Error(
        `match branch missing for ${String(tagKey)} = ${String(tag)}`
      );
    }
    return branchForTag(input);
  };

  return executeBranch;
};
```

先程定義した `UnionRecord` 型が型引数 `TUnion` の型制約として登場していますね。
これにより、 `input: TUnion` 引数には先程触れたタグの制約を満たすオブジェクトのみしか受け付けないようになります。
さらに、型引数 `TTagKey` の型制約には `TUnion` のキーであることが指定されています。
これにより、実在しないプロパティをタグのキーとして指定された場合にエラーを検出できるようになっています。

`match` 関数を呼び出す際、型引数 `TTagKey` の値は引数 `tagKey` の値を元に型推論されます。
例えば、 `match(status, "kind")` と呼び出した場合は `TTagKey = "kind"` と型推論されます。
この `TTagKey` の値はさらに型引数 `TUnion` の型制約 `UnionRecord<TTagKey>` にも使われ、引数 `input` に適切なタグのキー・値が存在することのチェックが行われます。

続けて、2回目に呼び出される関数（`executeBranch`）に型情報を追加していきます。
先程より若干複雑なので、まずは引数 `branches` にのみ型情報を追加し、その上で戻り値に型情報を追加するという2段階に分けて考えていきましょう。

```diff_typescript
export const match = <
  TUnion extends UnionRecord<TTagKey>,
  TTagKey extends keyof TUnion
>(
  input: TUnion,
  tagKey: TTagKey
) => {
-  const executeBranch = (branches) => {
+  const executeBranch = <TBranches extends MatchBranches<TUnion, TTagKey>>(
+    branches: TBranches
+  ) => {
    const tag = input[tagKey];
    const branchForTag = branches[tag];
    if (!branchForTag) {
      throw new Error(
        `match branch missing for ${String(tagKey)} = ${String(tag)}`
      );
    }
    return branchForTag(input);
  };

  return executeBranch;
};
```

型引数 `TBranches` の型制約に使われている `MatchBranches` は以下のように定義されます。

```ts
export type MatchBranches<
  TUnion extends UnionRecord<TTagKey>,
  TTagKey extends keyof TUnion
> = {
  [TagValue in TUnion[TTagKey]]: (
    value: VariantForTag<TUnion, TTagKey, TagValue>
  ) => unknown;
};
```

ここでは、入力オブジェクトの型のタグのキーを元に、 [Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) を使って2回目の関数呼び出しの引数の型を導き出しています。
`TUnion[TTagKey]` はタグのとり得る値を表すユニオン型に相当し、今回の例では `"subscribed" | "subscriptionStopped" | "neverSubscribed"` となります。
そのユニオンの構成要素それぞれを `TagValue` として取り出しながら、その `TagValue` をキー、 `(value: VariantForTag<TUnion, TTagKey, TagValue>) => unknown;` という関数を値とするオブジェクトを組み立てるのが、この `MatchBranches` 型です。
試しに今回の例を使って型を展開していくと、以下のようになります。

```ts
type MB1 = MatchBranches<Status, "kind">;
type MB2 = {
  [TagValue in "subscribed" | "subscriptionStopped" | "neverSubscribed"]: (value: VariantForTag<Status, "kind", TagValue>) => unknown,
}
type MB3 = {
  subscribed: (value: VariantForTag<Status, "kind", "subscribed">) => unknown,
  subscriptionStopped: (value: VariantForTag<Status, "kind", "subscriptionStopped">) => unknown,
  neverSubscribed: (value: VariantForTag<Status, "kind", "neverSubscribed">) => unknown,
}
```

`VariantForTag` 型についても見てみましょう。

```ts
export type VariantForTag<
  TUnion extends UnionRecord<TTagKey>,
  TTagKey extends keyof TUnion,
  TTagValue extends TUnion[TTagKey]
> = Extract<
  TUnion,
  {
    [TagKey in TTagKey]: TTagValue;
  }
>;
```

この型では、入力オブジェクトの型 `TUnion` から、タグのキーが `TTagKey` 、値が `TTagValue` である型のみを抜き出す（`Extract`）型操作を行っています。
今回の例でタグの値が `subscribed` の場合を使って型を展開していってみましょう。

```ts
type VFT1 = VariantForTag<Status, "kind", "subscribed">;
type VFT2 = Extract<Status, { kind: "subscribed" }>;
type VFT3 = Subscribed;
```

これら `MatchBranches` と `VariantForTag` の2つの型の合わせ技によって、 `branches` 引数の型安全性が担保され、プロパティの自動補完や網羅性検知が実現できているのです。

では、最後に `executeBranch` 関数の戻り値に型情報を追加してみましょう。

```diff_typescript
export const match = <
  TUnion extends UnionRecord<TTagKey>,
  TTagKey extends keyof TUnion
>(
  input: TUnion,
  tagKey: TTagKey
) => {
  const executeBranch = <TBranches extends MatchBranches<TUnion, TTagKey>>(
    branches: TBranches
-  ) => {
+  ): MatchOutput<TUnion, TTagKey, TBranches> => {
    const tag = input[tagKey];
    const branchForTag = branches[tag];
    if (!branchForTag) {
      throw new Error(
        `match branch missing for ${String(tagKey)} = ${String(tag)}`
      );
    }
-    return branchForTag(input);
+    return (
+      branchForTag as (input: TUnion) => MatchOutput<TUnion, TTagKey, TBranches>
+    )(input);
  };

  return executeBranch;
};
```

戻り値の型に使われている `MatchOutput` 型の定義は以下のとおりです。

```ts
export type MatchOutput<
  TUnion extends UnionRecord<TTagKey>,
  TTagKey extends keyof TUnion,
  TBranches extends MatchBranches<TUnion, TTagKey>
> = ReturnType<TBranches[keyof TBranches]>;
```

ここでは、 `branches` 引数の型として推論された `TBranches` 型を3つ目の型引数として受け取り、その戻り値を `ReturnType` 型を使って抜き出しています。
先程定義した `MatchBranches` ではそれぞれの関数の戻り値は `unknown` となっていましたが、2回目の関数呼び出し時に `branches` 引数を元に型推論される `TBranches` 型にはそれぞれの関数の具体的な戻り値の型が入っているため、 `MatchOutput` で抜き出される型もその具体的な型を元に抜き出されることとなります。
例えば、以下の使用例をご覧ください。

```ts
const result = match(status, "kind")({
  subscribed: () => "string",
  subscriptionStopped: () => 42,
  neverSubscribed: () => /regexp/,
});
type ResultType = typeof result;
// → type ResultType = string | number | RegExp
```

それぞれの関数の戻り値が異なり、かつどこにも明示的に型を指定している部分はありませんが、それでも型推論の結果 `result` 変数の型を `string | number | RegExp` として正しく推論できています。

以上が、Rust風 `match` 関数TypeScript版の定義の解説となります。
今回紹介したコードは [GitHubに公開してあるコード](https://github.com/uzawa-sorich/minrs/blob/main/src/keywords/match.ts) を元にしていますので、興味のある方はそちらもご覧ください。

# あとがき

今回紹介した `match` 関数は[npmパッケージminrs](https://www.npmjs.com/package/minrs)（[GitHubレポジトリ](https://github.com/uzawa-sorich/minrs)）として公開しています。
これから少しずつ機能を追加していく予定ではありますが、現時点ではまだ単体テストもない状態のためパッケージの品質は担保できないのが正直なところです。
また、このパッケージはTypeScript・Rustの機能紹介を目的とする思考実験的な位置づけにすぎないので、将来的に実用レベルのパッケージに育んでいくという思惑は今のところありません。

この記事を読んでご自身のJavaScriptプロジェクトにも `match` 関数のようなものがほしいと思った方も、現時点ではminrsパッケージに依存することはおすすめしていません。
その代わりに、 [match.ts](https://github.com/uzawa-sorich/minrs/blob/main/src/keywords/match.ts) を（必要に応じて自己流にアレンジした上で）プロジェクトに追加していただくことをおすすめします。