TypeScript: TupleToObject<T> ─ キーバリューのタプルからオブジェクトの型を作る方法


## やりたいこと

次のような`[string, any]`な型が複数あるとき、

```typescript
type Foo = ['foo', string]
type Bar = ['bar', number]
type Buz = ['buz', boolean]
```

これらをもとにして、次のようなオブジェクト型を導き出したい。

```typescript
type TheObject = {
    foo: string
    bar: number
    buz: boolean
}
```

## やりかた

`TupleToObject`というタプル型からオブジェクト型にマッピングする型を便宜上作ると良い。

```typescript
type TupleToObject<T extends [string, any]> = {
    [K in T[0]]: Extract<T, [K, any]>[1]
}
```

この型の使い方は、`TupleToObject`の型パラメータにタプル型をユニオン型で渡す。

```typescript
type TheObject = TupleToObject<Foo | Bar | Buz>
```

## 動作確認

期待通りのオブジェクト型が導出されているか確認してみる。

```typescript
// これがコンパイルに通り、
const obj1: TheObject = { foo: '', bar: 0, buz: false }

// これがコンパイルエラーになれば期待通り。
const obj2: TheObject = { foo: 0, bar: false, buz: '' }
```

## `TupleToObject`の仕組み

どのように`TupleToObject`が動くのかを見てみよう。

`TupleToObject`の定義をもう一度見てみると、`Extract`を使っているのが分かる。

```typescript
type TupleToObject<T extends [string, any]> = {
    [K in T[0]]: Extract<T, [K, any]>[1]
}
```

`Extract`がタプルをオブジェクトに変換する上で、重要な役割なのだが、それを説明する前に、`TupleToObject`の失敗作を先に見せておきたい。

最初に作った失敗版`TupleToObject`は次のような定義になっていた。

```typescript
// 失敗作
type TupleToObject<T extends [string, any]> = {
    [K in T[0]]: T[1]
}
```

タプルの要素0をキーに、要素1を値にといった具合に率直な定義になっており、一見するとこれでも期待通りのオブジェクト型が導出できそうだ。しかし、実際使ってみると全然ダメだった。

この失敗作に`TupleToObject<Foo | Bar | Buz>`を当てはめてみると、値がすべてユニオン型になってしまったのだ。

```typescript
type TheObject = {
    foo: string | number | boolean;
    bar: string | number | boolean;
    buz: string | number | boolean;
}
```

そのため、コンパイルエラーにしたい次のようなコードも、コンパイルが通ってしまった。

```typescript
const obj2: TheObject = { foo: 0, bar: false, buz: '' }
```

この問題を解決してくれるのが、最初に重要だと述べた`Extract`だった。

```typescript
type Extract<T, U>
```

`Extract`は2つの型パラメータを取り、T型がU型に代入可能であれば、その型を返し、そうでなければ`never`を返すConditional Typeだ。

例えば、次のようなコードでは`X`の型は`string`になる。というのも、`string, number, boolean`のうち、`string`に代入できるのは、`string`だけだからだ。

```typescript
type X = Extract<string | number | boolean, string>
```

もちろん、タプル型にも応用できて、3つのタプルから特定のタプルを抽出することができる。

```typescript
type X = Extract<['foo', string] | 
                 ['bar', number] | 
                 ['buz', boolean], ['foo', any]>
````

ここでの`X`の型は`['foo', string]`になる。

この抽出の仕掛けを失敗作に適用することで、キーに該当する値の型を抽出することができるようになる。

```typescript
type TupleToObject<T extends [string, any]> = {
//  失敗作 
//  [K in T[0]]: T[1]
    [K in T[0]]: Extract<T, [K, any]>[1]
}
```

## 応用

キーバリューのタプルからオブジェクトの型を作る方法を応用すると、オブジェクトを合成したオブジェクトの型を作ることもできる。

```typescript
const increment = {
    name: 'increment',
    calculate: (value: number): number => value + 1
} as const

const add = {
    name: 'add',
    calculate: (a: number, b: number): number => a + b
} as const

const sum = {
    name: 'sum',
    calculate: (...nums: number[]): number => nums.reduce((a, b) => a + b, 0)
} as const

interface Calculate<Name extends string = string> {
    name: Name
    calculate(...nums: number[]): number
}

type CalculatorOf<Method extends Calculate> = {
    [Name in Method['name']]: Extract<Method, Calculate<Name>>['calculate']
}

type TheCalculator = CalculatorOf<typeof increment | typeof add | typeof sum>

const calculator: TheCalculator = {
    [increment.name]: increment.calculate,
    [add.name]: add.calculate,
    [sum.name]: sum.calculate,
}

calculator.increment(1)
calculator.add(1, 2)
calculator.sum(1, 2, 3, 4, 5)

calculator.increment(1, 1) // コンパイルエラー: 引数が多い
calculator.add(1) // コンパイルエラー: 引数が足りない
calculator.sum(true, false, 'str') // コンパイルエラー: 引数の型が正しくない
```

## 参考資料

* [Tuple to Object in TypeScript via generics - Stack Overflow](https://stackoverflow.com/questions/56988970/tuple-to-object-in-typescript-via-generics)
* [TypeScript特有の組み込み型関数 - log.pocka.io](https://log.pocka.io/posts/typescript-builtin-type-functions/#extract)