TypeScript: index signatureを持つインターフェイスにkeyofして、既知のキーだけを取り出したい

この投稿では、TypeScriptの次のような問題と理想とその理想を実現する手法を紹介します。

* 問題点: `keyof`をインデックスシグネチャを持つインターフェイスに対して使うと、`string | number`になってしまう。
* 理想: `string | number`じゃなくて、インデックスシグネチャ以外の既知キーを知りたい。

## `keyof`は良い :yum:

TypeScriptには`keyof`というものがあり、インターフェイスが持つキーを調べることができます:

```typescript
interface Foo {
    a: number
    b: number
    c: number
}

type FooKeys = keyof Foo
//=> "a" | "b" | "c"
```

オブジェクトのキーを値で扱うときに、タイポに気づきやすく安全になったり、コード補完も効いたりして便利です:

```typescript
let fooKeys: keyof Foo
fooKeys = 'a' // OK
fooKeys = 'b' // OK
fooKeys = 'c' // OK
fooKeys = 'd' // Error: dなんて無いよと教えてもらえる！
```

## `keyof`をインデックスシグネチャに使うと良さが半減 :cry:

ところが、インデックスシグネチャを持つインターフェイスに対して、`keyof`をすると、`string | number`のようなざっくりとした型になってしまうので、:

```typescript
interface Bar {
    a: number
    b: number
    c: number
    [key: string]: any // インデックスシグネチャ
    [key: number]: any // インデックスシグネチャ
}

type BarKeys = keyof Bar 
//=> string | number
```

前述したような安心感が半減します:

```typescript
let barKeys: keyof Bar //=> string | number
barKeys = 'a' // OK
barKeys = 'b' // OK
barKeys = 'c' // OK
barKeys = 'd' // OK (´・ω・｀)
barKeys = 1 // OK (´・ω・｀)
```

`Bar`インターフェイスは「キーは`string`か、`number`だったら何でもいいよ」と定義しています。

なので、上のコードのように未知のキーを扱えるのは、論理的には正しいです。

## 既知のキーだけ取り出す`KnownKeyOf<T>`を作る :innocent:

とは言っても、実用上、既知のキーだけを知りたいことがあります。

理想的にはこうしたい:

```typescript
let barKnownKeys: KnownKeyOf<Bar> //=> "a" | "b" | "c"
barKnownKeys = 'a' // OK
barKnownKeys = 'b' // OK
barKnownKeys = 'c' // OK
barKnownKeys = 'd' // Error: dなんて知らないよと教えてもらえる 😊
barKnownKeys = 1 // Error: 1なんて知らないよと教えてもらえる 😊
```

これを叶えてくれる`KnownKeyOf<T>`は次のような実装になります:

```typescript
type KnownKeyOf<T> = {
  [K in keyof T]: string extends K ? never : 
                  number extends K ? never : K
} extends { [_ in keyof T]: infer X }
  ? X
  : never
```

### `KnownKeyOf<T>`の解説

ちょっと込み入ったコードになるので説明します。

`KnownKeyOf<T>`は2つのパートに分けられます。

1つ目はこの部分:

```typescript
{
  [K in keyof T]: string extends K ? never : 
                  number extends K ? never : K
}
```

これは、`T`インターフェイスを、`{ キー名: 値の型 }`から`{ キー名: キー名 }`に変換する部分です。ただ、「値の型」から「キー名」に変換するとき、細工があって、「キー名」がリテラル型**じゃない**のときだけ、`{ キー名: never }`にするようになっています。つまり、インデックスシグネチャのキーだけ`never`で目印をつけているわけです。例えば、`Bar`をこの部分に当てはめると、次のような構造のインターフェイスが導出されます:

```typescript
type Part1 = {
  [K in keyof Bar]: string extends K ? never : 
                    number extends K ? never : K
}
// ↓
type Part1 = {
    [x: string]: never; // インデックスシグネチャだけ
    [x: number]: never; // neverになる！
    a: "a";
    b: "b";
    c: "c";
}
```

`Part1`に続く、2つ目のパートはここ:

```typescript
Part1 extends { [_ in keyof T]: infer X }
  ? X
  : never
```

これは、`Part1`で`never`の目印をつけたキーを除去する部分です。`infer X`は、`{ キー名①: キー名② | never }`の「キー名②」部分を`X`に代入します。この代入の段階で、`never`が自動的にはじかれ、既知のキー名だけに絞り込まれます。

## 備考

複雑なインターフェイスでも期待通り動くことは確認済みです:

```typescript
type Complex = {
    a: number
    readonly b: string
    readonly c?: boolean
    d: number | string
    [key: string]: any
}

type KnownKeyOfComplex = KnownKeyOf<Complex>
//=> type KnownKeyOfComplex = "a" | "b" | "c" | "d"
```

```typescript
type ComputedKeys = {
    [key in 'a' | 'b']: string
} & {
    [key in 1 | 2 ]: string
} & {
    [key: string]: string
    [key: number]: string
}
type KnownKeyOfComputedKeys = KnownKeyOf<ComputedKeys>
//=> type KnownKeyOfComputedKeys = "a" | "b" | 1 | 2
```