関数のパラメータと返却値の制限

## やりたいこと

ダイアログを表示する `DoDialog()` とそのパラメータ `DialogOptionType` があるとします。そして、`DialogOptionType` にはボタンの定義があり、`name`（表示する文字列）と `id` （押されたボタンの識別子）が定義されており、`DoDialog()` を実行すると `id` が返却される仕組みを考え、これをサンプルコードにするとこんな感じになります。

```ts
type DialogOptionType = {
  title: string;
  message: string;
  buttons:  {
    id: string;
    name: string;
  } [];
};

function DoDialog(option: DialogOptionType) : Promise<string> {
  return new Promise<string>((resolve, reject) => {
    /** 
     * 実際には表示処理とユーザ入力処理が実行され、その結果を以て resolve するが、
     * これはサンプルなので１個目のボタンが押されたのを想定して resolve する。
     **/
    option.buttons.length == 0 ?
      reject(Error("ボタンが無い")) :
      resolve(option.buttons[0].id);
  });
}

DoDialog({
  title: "問題です",
  message: "１＋１は３ですか？",
  buttons: [
    { id: "yes", name: "はい" },
    { id: "no", name: "いいえ" }
  ]
})
.then((result) => {
  if(result == "yes") console.log("不正解");
  else if(result == "no") console.log("正解");
  else console.log(`resultが想定外 ${result}`);
});
```

```:結果
不正解
```

これはこれで良いのですが、 `result` が `string` になってしまうので、リファクタリング中に `id` を変更しても問題が気付きにくいです。

たとえばこんな感じに、良かれと思ったリファクタリングが逆にバグを混入する結果になる場合があります。

```ts:リファクタリング（バグ混入）
DoDialog({
  title: "問題です",
  message: "１＋１は３ですか？",
  buttons: [
    { id: "Yes", name: "はい" },
    { id: "No", name: "いいえ" }
  ]
})
.then((result) => {
  if(result == "yes") console.log("不正解");
  else if(result == "no") console.log("正解");
  else console.log(`resultが想定外 ${result}`);
});
```

```:結果
resultが想定外 Yes
```

まぁ、ちゃんと　`else` 節を作って `result` が想定外であることを示しているので、ある意味エラー処理をしているとも言えます。

が、しかし、
<b>「この問題が発覚するのは、このコードが実行された時」</b>
というのは如何なものかと思います。[^1]

[^1]: テスト駆動型開発にすれば良いという話もありますが、ここではテストを実施する以前に何とかならないかということです。それに、どんな場合であれテストは重要です。

ここで `result` を `string` ではなく `"yes" | "no"` （リファクタリング後は `"Yes" | "No"` ）のようにストリングリテラル型にできれば、コンパイルエラーとなるので問題が未然に防げるので、その方法を模索します。


## 色々と実験してみる

では、値（配列の特定プロパティ `id` ）から型（`id`を構成するストリングリテラル型）を生成する方法を模索してみようと思います。


### その１

問題を単純化するために下記のように定義します。

```ts
type OptionType = {id: string}[];
const options: OptionType = [{id: "yes"}, {id: "no"}];
type IdsType = typeof options[number]["id"];
/* IdsType: string */
```
やりたいことは `options` 内の各オブジェクトの `id` をストリングリテラル型にして `"yes" | "no"` にしたいので、こんな感じで書いみましたが、結果はダメです。
`options` は `const` なので、 `options` に代入はできないものの、中身を変更することは可能 [^2] なので、コンパイル時に定数とないからです。

[^2]: https://typescriptbook.jp/reference/values-types-variables/object/readonly-vs-const

では、こうしてみます。

```ts
type OptionType = {id: string}[];
const options: OptionType = [{id: "yes"}, {id: "no"}] as const; /* <- error */
type IdsType = typeof options[number]["id"];
```

これもダメです。
この場合、`options` の初期化で代入しようと定義している値の型が下記のようになるためコンパイルエラーとなります。
```ts
readonly [{ readonly id: "yes"; }, { readonly id: "no"; }]
```

そこで、ひとまず、こうすればエラーにはならず、`"yes" | "no"` も抽出できます。

```ts
const options = [{id: "yes"}, {id: "no"}] as const;
type IdsType = typeof options[number]["id"];
/* IdsType: "yes" | "no" */
```

が、しかし、これでは `options` に型指定ができないので気に入りません。最終的には `DoDialog()` のパラメータとしたいのですからね。

### その２

ここで趣向を変えて、オブジェクトの配列内の特定のキーをストリングリテラル型にする方法を考えてまみす。

```ts
type ARR = [{id: "yes"}, {id: "no"}];
type ITEM = ARR extends (infer U)[] ? U : never;
/* ITEM: {id: "yes"} | {id: "no"} */
type IDS = "id" extends keyof ITEM ? ITEM["id"] : never;
/* IDS: "yes" | "no" */
```

ということは、ジェネリクスでまとめるとこんな感じになります。

```ts
type ObjectArrayKeyValuesType<KEY, ARR> =
  ARR extends (infer ITEM)[] ?
    KEY extends keyof ITEM ?
      ITEM[KEY] 
    : never
  : never;
```

実際に試してみると。。。

```ts
type IDS = ObjectArrayKeyValuesType<"id", [{id: "yes"}, {id: "no"}]>;
/* IDS: "yes" | "no" */
```

`Object～<>` とは云うものの、実際は型なので命名がイマイチですが、期待する結果になります。
あ、でも `ARR` が可変である場合はストリングリテラル型ではなく `string` になります。

```ts
const VALUES = [{id: "yes"}, {id: "no"}];
type IDS = ObjectArrayKeyValuesType<"id", typeof VALUES>;
/* IDS: string */
```

コンパイル時に値が確定していないから当たり前ですね。
ではこうしてみたらどうでしょう？

```ts
const VALUES = [{id: "yes"}, {id: "no"}] as const;
type IDS = ObjectArrayKeyValuesType<"id", typeof VALUES>;
/* IDS: never */
```

ん？！　なぜ `never` ？

ジェネリクスでは参考演算子を連鎖しているので、どこで `never` 節を採用するのかを確認すべく、ジェネリクスを再度分解して確認してみましょう。

```ts
const VALUES = [{id: "yes"}, {id: "no"}] as const;
type ARR = typeof VALUES;
/* ARR: readonly [{ readonly id: "yes"; }, { readonly id: "no"; }] */
type ITEM = ARR extends (infer U)[] ? U : never;
/* ITEM: never */
type IDS = "id" extends keyof ITEM ? ITEM["id"] : never;
/* IDS: never */
```

どうやら、 `ARR extends (infer U)[]` がダメみたいです。[^3]
`ARR` の違いを考えると恐らく `readonly` が関係してそうだって事で、`readonly` の場合分けで試してみることにします。

[^3]: こういうのは、後でちゃんと理由を深堀りすること！

```ts
/* これが元となるコード */
type ARR = [{ id: "yes"; }, { id: "no"; }];
type ITEM = ARR extends (infer U)[] ? U : never;
/* ITEM: { id: "yes"; } | { id: "no"; } */
type IDS = "id" extends keyof ITEM ? ITEM["id"] : never;
/* IDS: "yes" | "no" */

/* readonly の組み合わせを試してみる */

type ARR_1 = readonly [{ readonly id: "yes"; }, { readonly id: "no"; }];
type ITEM_1 = ARR_1 extends (infer U)[] ? U : never;
/* ITEM_1: never */
type IDS_1 = "id" extends keyof ITEM_1 ? ITEM_1["id"] : never;
/* IDS_1: never */

type ARR_2 = [{ readonly id: "yes"; }, { readonly id: "no"; }];
type ITEM_2 = ARR_2 extends (infer U)[] ? U : never;
/* ITEM_2: { readonly id: "yes"; } | { readonly id: "no"; } */
type IDS_2 = "id" extends keyof ITEM_2 ? ITEM_2["id"] : never;
/* IDS_2: "yes" | "no" */

type ARR_3 = [{ id: "yes"; }, { readonly id: "no"; }];
type ITEM_3 = ARR_3 extends (infer U)[] ? U : never;
/* ITEM_3: { id: "yes"; } | { readonly id: "no"; } */
type IDS_3 = "id" extends keyof ITEM_3 ? ITEM_3["id"] : never;
/* IDS_3: "yes" | "no" */

type ARR_4 = readonly [{ id: "yes"; }, { id: "no"; }];
type ITEM_4 = ARR_4 extends (infer U)[] ? U : never;
/* ITEM_4: never */
type IDS_4 = "id" extends keyof ITEM_4 ? ITEM_4["id"] : never;
/* IDS_4: never */
```

これを見ると、配列が `readonly` だと `infer` が使えないんじゃないかという結果になりました。配列の中のプロパティについては　`readonly` は関係ないようです。[^3]

### その３

「その１」と「その２」を比較することにしましょう。
「その１」では、下記のような取り扱いとなるという結果でした。

```ts
const options = [{id: "yes"}, {id: "no"}] as const;
type IdsType = typeof options[number]["id"];
/* IdsType: "yes" | "no" */
```

ここで、「その２」の記述に寄せて書くと下記のようになります。

```ts
const VALUES = [{id: "yes"}, {id: "no"}] as const;
type ARR = typeof VALUES;
/* ARR: readonly [{ readonly id: "yes"; }, { readonly id: "no"; }] */
type IDS = ARR[number]["id"];
/* IDS: "yes" | "no" */
```

更に、変形するとこうとも書けるでしょう。

```ts
type ARR = readonly [{ readonly id: "yes"; }, { readonly id: "no"; }];
type IDS = ARR[number]["id"];
/* IDS: "yes" | "no" */
```

ここで「その２」の検証では下記の結果でした。

```ts
type ARR = readonly [{ readonly id: "yes"; }, { readonly id: "no"; }];
type ITEM = ARR extends (infer U)[] ? U : never;
/* ITEM : never */
type IDS = "id" extends keyof ITEM ? ITEM["id"] : never;
/* IDS: never */
```

これをまとめると下記の結果になります。

```ts
type ARR = readonly [{ readonly id: "yes"; }, { readonly id: "no"; }];

/* その１ */
type IDS_CASE_1 = ARR[number]["id"];
/* IDS_CASE_1: "yes" | "no" */

/* その２ */
type ITEM_CASE_2 = ARR extends (infer U)[] ? U : never;
/* ITEM_CASE_2: never */
type IDS_CASE_2 = "id" extends keyof ITEM_CASE_2 ? ITEM_CASE_2["id"] : never;
/* IDS_CASE_2: never */
```

ところで、 `ARR` の配列に修飾する `readonly` を除いてみるとどうなるか試してみます。

```ts
type ARR = [{ readonly id: "yes"; }, { readonly id: "no"; }];

/* その１ */
type IDS_CASE_1 = ARR[number]["id"];
/* IDS_CASE_1: "yes" | "no" */

/* その２ */
type ITEM_CASE_2 = ARR extends (infer U)[] ? U : never;
/* ITEM_CASE_2: { readonly id: "yes"; } | { readonly id: "no"; } */
type IDS_CASE_2 = "id" extends keyof ITEM_CASE_2 ? ITEM_CASE_2["id"] : never;
/* IDS_CASE_2: "yes" | "no" */
```

なるほど、「その２」で作った `ObjectArrayKeyValuesType` で `typeof` を使用するためには配列の `readonly` を外すことができれば良さそうですが。。。


### 実験の結果

値から型を抽出して変形するというのは手法は不可能ではありません。
しかし、対象となる値は不変であることを要求するため、気軽に `DoDialog()` のパラメータとして使用するには向かないでしょう。

そこで、別な方法を検討することにします。


## 別の方法を試す

これまで `DialogOptionType.buttons[]` の `id` をストリングリテラル型に変換しようと苦戦していましたが、逆に `id` の型を制限する（許容される値を制限する）方針に変更してみます。

つまり、こんな感じになります。

```ts:元のコード
type DialogOptionType = {
  title: string;
  message: string;
  buttons:  {
    id: string;
    name: string;
  } [];
};
```

```ts:idに制約を設ける
type DialogOptionType<T> = {
  title: string;
  message: string;
  buttons: {
    id: T;
    name: string;
  } [];
};
```

ここで `T` はストリングリテラル型が入り、 `"yes" | "no"` を指定することで、`DialogOptionType` の値を定義する際に静的エラーチェックができるようになります。

```ts
const opt: DialogOptionType<"yes" | "no"> = {
  title: "問題です",
  message: "１＋１は３ですか？",
  buttons: [
    { id: "Yes", name: "はい" },  /* <- コンパイルエラーになります */
    { id: "no", name: "いいえ" }
  ]
};
```

このままでは `DoDialog()` のパラメータにはできないので、 `DoDialog()` をジェネリクス関数にして `"yes" | "no"` を `DialogOptionType` に引き渡します。

```ts
function DoDialog<T>(option: DialogOptionType<T>) : Promise<T> {
  return new Promise<T>((resolve, reject) => {
    option.buttons.length == 0 ?
      reject(Error("ボタンが無い")) :
      resolve(option.buttons[0].id);
  });
}
```

こうすることで、下記のような呼び出しで型安全になります。

```ts
DoDialog<"yes" | "no">({
  title: "問題です",
  message: "１＋１は３ですか？",
  buttons: [
    { id: "yes", name: "はい" },
    { id: "no", name: "いいえ" }
  ]
})
.then((result) => { /* result: "yes" | "no" */
  if(result == "yes") console.log("不正解");
  else if(result == "no") console.log("正解");
  /* else は不要 */
});
```

タイプ量は若干増えます[^4] が、型チェックによる恩恵は計り知れないものがあります。

[^4]: まぁ、これを節約しようと試みて失敗していますが（苦笑）

```ts
type DialogOptionType<T> = {
  title: string;
  message: string;
  buttons: {
    id: T;
    name: string;
  } [];
};

function DoDialog<T>(option: DialogOptionType<T>) : Promise<T> {
  return new Promise<T>((resolve, reject) => {
    option.buttons.length == 0 ?
      reject(Error("ボタンが無い")) :
      resolve(option.buttons[0].id);
  });
}

DoDialog<"yes" | "no">({
  title: "問題です",
  message: "１＋１は３ですか？",
  buttons: [
    { id: "yes", name: "はい" },
    { id: "no", name: "いいえ" }
  ]
})
.then((result) => {
  if(result == "yes") console.log("不正解");
  else if(result == "no") console.log("正解");
});
```

## まとめ

コンパイル時に値が確定している場合、その値を使用するコードに制限を設けることで、値とコードの不整合をコンパイル時に見つけられるようにしましょう。