TypeScriptで文字列からキーを抽出し、オブジェクトの型に変換する方法

はいさい！ちゅらデータぬオースティンやいびーん！

# 概要
TypeScriptで文字列からキーを抽出し、オブジェクトの型（`type`）に変換する方法を紹介します。

## 背景
Express.jsなどのフレームワークで以下のようなコードをよく書きます。
```typescript
app.get("/:id", (req, res) => res.send(req.params.id));
```
読者に気づいた方はいらっしゃると思いますが、この`req.params.id`は、TypeScriptで書いていると、`req.params`の型に含まれています。

つまり、`req.params`は以下のような型になっています。
```typescript
type Params = { id: string } & { [name: string]: string};
```
`req.params.id`をVS Codeで呼ぼうとすると、intellisenseで推測してくれるのではないでしょうか？

`"/:id"`と書いただけで、なぜTypeScriptは推測できるようになっているのだろうか気になりませんか？

どうなっているのか、以下解説します。

# TypeScriptの技術
上記の推測を実現するためにはTypeScriptの4つの機能が使われています。

## Generic Types　（ジェネリック型）
TypeScriptをよくご存知の方ならば、一度は使って遊んだことがあるかと思います。

https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-types

例えば、以下のように`doNothing`という、引数を返すだけの関数を定義するとしましょう。

引数の型に合わせて返す値の型を設定したいです。

1. 文字列を引数に渡したら、文字列を返す
1. 数値を引数に渡したら、数値を返す

すると、Generic Typesを使うと、上記のような型を書くことができます。
```typescript
function doNothing<T>(value: T): T {
  return value
}

const doNothingArrow = <T>(value: T) => value; // Arrow関数の場合はこのような構文
```
これはよく<a href="https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads">Function Overloads</a>で使用されますが、今回は下記の技術と一緒に使います。

## Template Literal Types (テンプレート・リテラル型)
実は、TypeScipt v4.1から、文字列から型を抽出する機能が入っていたのです。

https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

上記の正式ドキュメントの説明を借りますと、
```typescript
type World = "world";
 
type Greeting = `hello ${World}`; // Greeting: "hello world"
```
このように、型の定義に違う型をはめられることが後々役立ってきます。

## Conditional Types　（条件的型）
次のTypeScript技術は、条件によって性質が変わる型の定義です。

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#conditional-types

上記の正式ドキュメントの例を拝借しますと、以下のような型を書くことができます。
```typescript
type TypeName<T> = T extends string ? string : unknown;
type T0 = TypeName<string>; // type T0 = string
type T1 = TypeName<"a">; // type T1 = string
type T2 = TypeName<number> // type T2 = unknown
type T3 = TypeName<1> // type T3 = unknown
```
`T`というGeneric Typeを定義し、それがもし`string`という型から成っていれば、`string`として認めるが、それ以外だと、わからないという意味で`unknown`にします。

すると、文字列をGeneric Typeの引数に渡して`T1`という型を定義すると、その型定義の結果が`string`になります。それ以外の値だと、`unknown`になります。

この機能もまた、後ほど役立ちますので、脇に置いておきましょう。

## 条件的型におけるType inference　（型推測）
最後に必要なTypeScript機能は、条件的型の定義に使える、型を推測する機能です。

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types

正式ドキュメントの例ですが、以下のようにオブジェクトの中に入っている`a`と`b`のキーの値の型を推測し、型として定義しています。
```typescript
type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
type T10 = Foo<{ a: string; b: string }>; // string
type T11 = Foo<{ a: string; b: number }>; // string | number
```
<strong>型から、型を抽出する</strong>、これを覚えていただければと思います。

# ExpressのParam推測を自作してみる
これからは、上記でご紹介したTypeScript機能を使い、Expressのパラメーターの型を推測する機能を再現してみます。

## `:`が入っている文字列から型を抽出する
まず、Expressの場合、`:`が入っている文字列のみ、パラメーターとして認定しているので、`:`が入っているかどうかをチェックする条件的型を定義します。
```typescript
type GetTypeIfHasColon<T> = T extends `:${infer U}` ? U : never;

type T0 = GetTypeIfHasColon<":aa">; // "aa"
type T1 = GetTypeIfHasColon<"aa">; // never

```
ここで、テンプレート・リテラルで、正規表現のようなチェックをしています。正規表現だと、`/:+/`のような書き方になるかと思います。

その正規表現チェックが通れば、そのセミコロンを省いた文字列の型を推測し、返す。でなければ、`never`を返す。

ちなみに、<a href="https://typescript-jp.gitbook.io/deep-dive/type-system/never">`never`</a>は、あり得ない処理、あり得ない結果の時に使われる型。この場合は、`never`を使って、TypeScriptに、セミコロンのパターンに一致しなかったら、無視していいと指定しています。

## `/`が入っている文字列から型を抽出する
次に解決しないといけない問題は、Expressのパスに入っているバックスラッシュ`/`をどうするかです。

まず、上記と同じように、テンプレート・リテラルで型を推測するようにしましょう。
```typescript
type SplitPath<T> = T extends `${infer U}/${infer V}` ? U | V : T;

type T2 = SplitPath<"/:aa"> // "" | ":aa"
type T3 = SplitPath<":aa"> // ":aa"
type T4 = SplitPath<"/:aa/data"> // "" | ":aa/data"
```
`T4`の結果が、望ましくないですね。ここで必要なのは、再起的な処理です。
```typescript
type SplitPathRecursive<T> = T extends `${infer U}/${infer V}` ? U | SplitPathRecursive<V> : T;

type T2 = SplitPathRecursive<"/:aa"> // "" | ":aa"
type T3 = SplitPathRecursive<":aa"> // ":aa"
type T4 = SplitPathRecursive<"/:aa/data"> // "" | "data" | ":aa"
```
こうすると`/`がたくさんあっても検知できるようになりました！

## `:`と`/`のテンプレート・リテラル型を合わせる
最後に、上記の`SplitPathRecursive`に`GetTypeIfHasColon`を組み込みます。
```typescript
type SplitPathRecursive<T> = T extends `${infer U}/${infer V}`
  ? GetTypeIfHasColon<U> | SplitPathRecursive<V>
  : GetTypeIfHasColon<T>;

type T2 = SplitPathRecursive<"/:aa">; // "aa"
type T3 = SplitPathRecursive<":aa">; // "aa"
type T4 = SplitPathRecursive<"/:aa/data">; // "aa"
```
これで、Expressらしくなってきましたね！ただ、Expressだと、`req.params`のように、オブジェクトになっていますので、もう少し頑張りましょう。

## `Record`の効用型を使う
文字列の型`"a"`を、オブジェクトのキーになる型を作ることができます。`Record`という、TypeScriptに内蔵されているUtility Type（効用型）を使います。
```typescript
type MyRecord = Record<"a", string> // { a: string }
```
これを上記の`SplitPathRecursive`と併用すると、パラメーターのキーにすることができます。値の型が必ず`string`になることもExpressの仕組み上わかっているので以下のようなコードを書きます。
```typescript
type ExpressInferredParams<T> = Record<SplitPathRecursive<T>, string>;

type T5 = ExpressInferredParams<"/:aa/data">; // { aa: string; }
const params: T5 = { aa: "Hello!" };
```
もう終わりが見えてきましたね。

## パラメーターで指定していないキーでも使えるようにする
Expressだと、`POST`リクエストのパラメーターはパスだけでないので、上記の定義だと、TypeScriptインタープリターが知らないパラメーターに対してエラーを吐き出すようになります。

解決するためには、以下のような型とユニオンにすることです。
```typescript
type ExpressInferredParams<T> = Record<SplitPathRecursive<T>, string>;
type ExpressParams<T> = ExpressInferredParams<T> & { [name: string]: string };

type T5 = ExpressParams<"/:aa/data">;
// ExpressInferredParams<"/:aa/data"> & {
//  [name: string]: string;
// }
const params: T5 = { aa: "1", picture: "abc" }
```
Expressと同じような結果、できましたね！

# まとめ
以上、ここまで、TypeScriptの様々な機能を使って、文字列からオブジェクトのキーを型に変換する方法を紹介してきましたが、いかがでしょうか？

実際、Expressのソースコードを見ていないので、このような実装になっているのか分かりませんが、きっと筆者以上に鋭い解決方法を使っているのではないかと思います！

TypeScriptのこういった機能をよく理解して使えるようになりたいものですね。