タプルをenumライクなオブジェクトに変換する

# 背景
## TypeScriptのenumを使うのはなるべく避けたい...
TypeScriptのenumはJavaScriptにはない機能です。
これはこれで便利なのですが、JavaScriptにトランスパイルされる際にJavaScriptにはない機能を使って変換されます。
特にenumの場合は以下のように[即時実行関数式](https://developer.mozilla.org/ja/docs/Glossary/IIFE)に変換されるので、バンドルサイズも大きくなってしまいます。

```ts:TypeScript
enum OperatingSystem {
  MacOS,
  Windows,
  Linux
};
```
```js:JavaScript
let OperatingSystem;
(function (OperatingSystem) {
    OperatingSystem[OperatingSystem["MacOS"] = 0] = "MacOS";
    OperatingSystem[OperatingSystem["Windows"] = 1] = "Windows";
    OperatingSystem[OperatingSystem["Linux"] = 2] = "Linux";
})(OperatingSystem || (OperatingSystem = {}));
; 
```

他にも様々なデメリットがあるようです。
詳しくは以下の記事で解説されています。

- https://www.kabuku.co.jp/developers/good-bye-typescript-enum
- https://engineering.linecorp.com/ja/blog/typescript-enum-tree-shaking/

## いちいちプロパティ名と値を書くのはなんか面倒だな
次のように書くとオブジェクトをenumのように使うことはできます。

```ts
const OperatingSystem = {
  MacOS: 0,
  Windows: 1,
  Linux: 2
} as const;
type OperatingSystem = typeof OperatingSystem[keyof typeof OperatingSystem];
```
しかし、プロパティ名と値の文字列が同じだったり、enumだと値を省略すれば0から値が順番に入っていたのを手作業で書くのを繰り返していくと、「俺はこんなことをするためにプログラミングをしたいんじゃないんだ...!」という気持ちに襲われます。

次のようにプロパティ名と値が微妙に異なるケースもあると思います。

```ts
const OperatingSystem = {
  MacOS: 'macOS', // 値はmacOSにしたい
  Windows: 'Windows',
  Linux: 'Linux'
} as const;
type OperatingSystem = typeof OperatingSystem[keyof typeof OperatingSystem];
```

# タプルをenumライクなオブジェクトに変換する関数を定義しよう
## 使用例
次のように引数にタプルを受け取り、それに基づくオブジェクトを生成する関数を定義したいです。
おまけに第2引数にオプションを指定するだけで値を数値にするか決められるようにしたいです。

```ts
const OperatingSystem = enumObject(['macOS', 'Windows', 'Linux'] as const);
const macOS = OperatingSystem.MacOS; // 'macOS'

const OperatingSystem = enumObject(['macOS', 'Windows', 'Linux'] as const, { index: true });
const macOS = OperatingSystem.MacOS; // 0
```

```ts
// 関数の戻り値の型はOperatingSystemの型は次のように生成されるようにしたい
type OperatingSystem = {
  readonly MacOS: 'macOS';
  readonly Windows: 'Windows';
  readonly Linux: 'Linux';
}
```
これは以下のようなユーティリティ型も実装しておくと非常に便利です。

```ts
type Values<T> = T[keyof T];
```
```ts
const OperatingSystem = enumObject(['macOS', 'Windows', 'Linux'] as const);
type OperatingSystem = Values<typeof OperatingSystem>;

let os: OperatingSystem; // 'macOS' | 'Windows' | 'Linux'
os = OperatingSystem; // Error!
os = OperatingSystem.Linux; // 'Linux'
```
## 実装
enumObject関数の定義は以下のとおりです。[Playground](https://www.typescriptlang.org/play?ts=4.1.2#code/N4Ag9GKNHyCMBQIQFMB2BXAtiA8gByQE4CGALgJYoDmAygJ4DOJSWwCiIAskQMbbUA0bRAHUKAEwD2Ad3qD2IADIU0ADzYBfANxx1cOKAjQATG24SUjHPmLkqdRsxABeZOgzYARgCsk3EgAoAbQByDB4+YP4QYNEUSRlI6KV0FWCAXRAiehAzCxIASm1EXMswlT5nK0JSChoGJgwAOi5eak1wSFDw6mCdPQNIKABmU3NLPGrbOocsF1RMTx8-IK7WxJjxaXp15NV0zOySkijQcSQVAC4QEgI0JBB1QtG8kDDWyombWvsG5u72wwABj6cAAZmgUH4yOZXAtvL4SAAeAAqyBUTDi2QASkgiJIUAAbWgAQQIxFoiMYBFqAD4af4SGhcASkFdkfkrgBRNzUG61FE07TgyHkGHzdzwvwotEYsTY3H4omk8mUvlUOkMpkstlRCS4UUWK6nOLnK43O4PDkgbmYABymA8hAFQohULFbkWCOl51l8rx5iVZKIFKptPpjOZrJAyN1+uhFgA-EaQGdLtdbvdHiBWMUxiQckRcGQSEQCWQAF73Fz+KlXUNUfLOGkgQJUwKAtKNEgSACquGsAGEskh-PkolTGvQy9wRzB8p2vBIKP5gsEnogCEhGQQUNctUhGpuxGgZ-5-BJvFFcAQ9VFU42nM2c+wL15AtxC8XSxWR9e9fPKj1A16ATRpUxABMUxNFQQCuP9cCKdhN23XdX0Q9Qok9PxGm4TdSBHYBHieXQ4EMBpmXwkAyyYYgCWuWh8HoQAkhkAf3lAHiGQArBkAEQZABYNQAIFTgEgGPuG0MG9dFUDlEAcX9QkSSDEM1UoOlKlAPDFVoFsAGkoOjQI3EdAg0jSK4dK0QThOtGBKlExEQhaCIog2OIth2ZRUjSQVSIAKgs-ArNUoQQHUgNNIc6grgAInCyLEI3BVQpAWJ4noKLkq2WKgpCuTFHcqLdhUTLdG8sA9CE-zRN5akqHE31pISuTlWDVVquU5sXDUhqiW03TkX0h1CAyLIQAAAwAEmAIcixLMtK0RLSaXUEaTJAMztHKkSjBsnklLs1ZsG2Jz0oSJyCvSLywF8jbrS2jqsq6sLwlSkBIreA7MvkbLuuO57Ip+j6kIe3KUny9yirgEqyss0T7QwQzaskv0NKaxTWpUjrgqBwIdIoEAAGskFoCRQWjEAADIQFhwylBo0sBQOUaJqmr9ZqQFFsc8paVrWvyRKGba7QGgg9renojs2E6knc87tEu3nrX5u7PqB8KrkBOLMdk76JeemANa+zSCquIxtGK0r5apwgaeqAkEcxeqtfklUiBQWgaSiHsZURkADMISpAXathUR9b3AjSNhIJ7Ng2S9+2DZbF3aCiRoU4oUE-ZxRhw-kSDLYIa3aMRTPjnqxhAkilkqBIAALSKMgAHxAHsaSCq4UCQAA3Qh1ss21Kjzgu6cCGAoiMKIhiiAAWTyAUgQEQEb6zG62xuRiAA)
注意点として、macOS -> MacOSのような型の変換にはTypeScript 4.1の[template literal types](https://github.com/microsoft/TypeScript/pull/40336)を使っています。

```ts
function enumObject<T extends ReadonlyArray<string>>(tuple: T): EnumString<T>;
function enumObject<T extends ReadonlyArray<string>>(tuple: T, options: { index: true }): EnumNumber<T>;
function enumObject<T extends ReadonlyArray<string>>(tuple: T, options?: { index: true }) {
  const capitalize = (str: string) => [str[0].toUpperCase(), str.slice(1)].join('');
  return tuple.reduce((obj, prop, index) => {
    obj[capitalize(prop)] = options?.index ? index : prop;
    return obj;
  }, Object.create({}));
}

type EnumString<T extends ReadonlyArray<string>>
 = { readonly [K in T[number] as `${Capitalize<K>}`]: K };

type EnumNumber<T extends ReadonlyArray<string>>
 = { readonly [K in keyof T & NumberLiteral<T> as `${Capitalize<T[K]>}`]: K };

type NumberLiteral<T extends ReadonlyArray<any>, U extends number = 0> =
  T extends []
  ? U
  : T extends [any, ...infer Rest]
    ? NumberLiteral<Rest, Rest["length"] | U>
    : never;
```

## 解説
まず、関数の型定義についてですが、この部分では[関数のオーバーロード](https://www.typescriptlang.org/docs/handbook/functions.html#overloads)を用いて、引数に応じて戻り値の型を分けてます。

```ts
// 引数がtupleだけのときはEnumString<T>
function enumObject<T extends ReadonlyArray<string>>(tuple: T): EnumString<T>;
// 第2引数が定義されているときはEnumNumber<T>
function enumObject<T extends ReadonlyArray<string>>(tuple: T, options: { index: true }): EnumNumber<T>;
```
`EnumString<T>`は[template literal typesとMapped Types内でのas句](https://github.com/microsoft/TypeScript/pull/40336)を使うことでプロパティ名をキャメルケース(パスカルケース)に変換しています。

```ts
type EnumString<T extends ReadonlyArray<string>>
 = { readonly [K in T[number] as `${Capitalize<K>}`]: K };
```
template literal typesを使わないバージョンは以下の通りです。
`T[number]`と書くことで配列TのUnion Typeを取得することができます。(`['a', 'b']` -> `'a' | 'b'`)

```ts
type Enum<T extends ReadonlyArray<string>> = { readonly [K in T[number]]: K };
```

`EnumNumber<T>`は配列の添字を使いたいので`T[number]`ではなく`keyof T`を使っています。
JavaScriptにおいて配列の実態はオブジェクトなので、`keyof T`には添字のプロパティの他に`length`や`push`なども含まれています。
そこでプロパティ`K`と数値リテラル型の積集合をとって`K`を添字のプロパティのみに絞り込んでいます。
ちなみに、数値リテラル型ではなく`number`とすればenumオブジェクトの値はすべて`number`型になってしまいます。

```ts
type EnumNumber<T extends ReadonlyArray<string>>
 = { readonly [K in keyof T & NumberLiteral<T> as `${Capitalize<T[K]>}`]: K };
```

数値リテラル型はTS4.1で制限が緩和されたRecursive Conditional Typesを用いて再帰的に数値リテラルのユニオン型を生成しています。

```ts
type NumberLiteral<T extends ReadonlyArray<any>, U extends number = 0> =
  T extends []
  ? U
  : T extends readonly [any, ...infer Rest]
    ? NumberLiteral<Rest, Rest["length"] | U>
    : never;

type N = NumberLiteral<[1, 2, 3, 'hoge']>; // 0 | 1 | 2 | 3
```
`infer`を用いてn-1の長さの配列を取得し、配列オブジェクトの`"length"`プロパティを参照することで配列の長さの数値リテラル型が返されるので、それを再帰的に`U`に入れていって、`T`が空になったところで`U`を返します。