Utility Type KnownKeys＜T＞がいつの間にか壊れていた話

# 概要
TypeScript にて、とあるオブジェクト型から一部のプロパティを取り除きたい場合、`Omit<Type, 'key1' | 'key2'>` で可能ですが、このとき `Type` が `{ [key: string]: string }` のようなインデックスキーを持っていると、 `{ [x: string]: any; [x: number]: any;}` になってしまうという問題があります。これを回避するため、インデックスキーを除いたキーのみを抽出する次のような `KnownKeys<T>` という Utility type を使っていました。(確か Stackoverflow かどこかから拝借したはずですが、失念、、、)

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

これを使って次のように書くことで、一部のプロパティのみを抜き出すことを実現していました。

```typescript
type IndexKeyType = {
  [key: string]: any
  key1: string
  key2: string
}
type Result = Omit<Pick<IndexKeyType, KnownKeys<IndexKeyType>, 'key2'>
// => { key1: string}
```

しかし、これが期待した結果ににならなくなっていたことが最近判明。本稿は、その調査の記録と解説になります。

# 原因

どうやら TypeScript 4.4.4 以降で発生している問題の模様。TypeScript の Project に Issue として投稿されています。

https://github.com/microsoft/TypeScript/issues/44143#issuecomment-843450189

一番最後のコメントに書かれていますが、4.4.4 以降で式の評価順序が変わってしまい、結果が変わってしまったということらしい。

https://github.com/microsoft/TypeScript/issues/44143#issuecomment-1406008044

# 修正方法

こちらのコメントの通りに修正することで解決。

https://github.com/microsoft/TypeScript/issues/44143#issuecomment-843450189

```typescript
type KeyToKeyNoIndex<T> = {
    [K in keyof T]: string extends K ? never : number extends K ? never : K;
};
type ValuesOf<T> = T extends { [K in keyof T]: infer U; } ? U : never;
type KnownKeys<T> = ValuesOf<KeyToKeyNoIndex<T>>;
```

前述した下記コメントにおいては、評価順序が変わってしまっただけなので `()` で評価順序を明示するだけで回避可能とありますが、我々の場合、コード成型で括弧が削除されてしまったので、上記の方法を採用しました。

https://github.com/microsoft/TypeScript/issues/44143#issuecomment-1406008044

# KnownKeys\<T> の仕組み

そもそも KnownKeys<T> はどのような仕組みで動いているのか、分解して解説していきます。幸い、修正後Verでは複数の型に分解されているので、その型ごとに説明。

## KeyToKeyNoIndex\<T>

まず、分解してみる

```
(1) { [ K in keyof T]: TYPE }
(2) string extends TYPE ? TRUE : FALSE
(3) K ? never : number extends K ? never : K
```

(1) は、ある型 `T` から、同じ key を持つ別の型を導出する際に使う手法。例えば、次のような感じ。

```typescript
type StringValueType = { a: string, b: string }
type NumberValuesType = { [ K in keyof StringValueType]: number }
// => { a: number, b: number}
```

(2) は型の3項演算子。今回の場合、`string` が `TYPE` の型制約を満たすのであれば TRUE に分岐する。この条件を満たす `TYPE` は `string` だけなので、つまり `TYPE === string` の評価をしていることと同義。
(1) と組み合わせると、`{ [K in keyof T]: string extends K ? TRUE : FALSE }` になるので、つまり key が string かどうかを評価している。

(3) は (2) と同じく3項演算子を使って、key が number かどうかを評価している。

もう一度全体を確認。

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

つまりここでやっていることは、とある型 `T` から `string` と `number` の key を取り除き、(正確には、value を never にする)、それ以外については key 名を value にコピーしている。

```typescript
type Result = KeyToKeyNoIndex<{
  [key: string]: any,
  [key: number]: any,
  aaa: any,
  5: any
}>
// =>  {
//    [x: string]: never
//    [x: number]: never
//    aaa: "aaa"
//    5: 5
}
```

## ValueOf\<T>

```typescript
type ValuesOf<T> = T extends { [K in keyof T]: infer U } ? U : never
```

[infer](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html) が登場。infer の詳しい説明は他の記事に譲るとして、ここではざっくり動きを説明。

まず、`T extends TYPE ? TRUE : FALSE` の３項演算子なので、型引数Tが TYPE が満たすかどうかを見ている。ここで出てくるのが infer。`infer U` により、extends の評価が真になった場合に、`infer U` が書かれていた位置に対応する型情報が `U` として解決される。つまり、この場合は `{ [K in keyof T]: infer U }` なので、型引数Tの各プロパティキーの値が `U` として解決される。

```typescript
type Result = ValuesOf<{ key1: string, key3: 123}>
// => string | 123
```

### KnownKeys\<T>

```typescript
export type KnownKeys<T> = ValuesOf<KeyToKeyNoIndex<T>>
```

上記2つの型の組み合わせになっている。まず、`KeyToKeyNoIndex<T>` により、インデックスキーが除去され、その他のキーにはキー名が値として設定される。

```typescript
type Step1 = KeyToKeyNoIndex<{
  [key: string]: any,
  [key: number]: any,
  aaa: any,
  5: any
}>
// =>  {
//    [x: string]: never
//    [x: number]: never
//    aaa: "aaa"
//    5: 5
}
```

これを `ValueOf<T>` に渡すことで、Value、つまり型引数Tのキー名の Union 型を導き出している。`never` は Union 型からは除去される。

```typescript
type Step2 = ValueOf<Step1> // Result
// => "aaa" | 5
```

# まとめ

TypeScript の更新によって型の非互換が生まれてしまっていたという問題でした。TypeScript のバージョンを 4.3 から上げたのはだいぶ昔なのですが、それを今になって気付いたというのはちょと状況としてよろしくないですね。理想的には型定義についても Test が書ければよいのですが、少し調べてみた限りそのような情報は見つかりませんでした。一応、次のようなコードで実現できないかなー？とも考えているのですが、スタンダードから外れた方法はあまりとりたくないという思いもあります。良い情報があればコメントで教えていただけると助かります。

```typescript
type AssertTypeEqual<T, U> = T extends U ? U extends T ? true : false : false
const testType = <T extends true>() => {}

testType<AssertTypeEqual<string, string>>() // Success
testType<AssertTypeEqual<string, number>>() // Type error
```

では、よい Generics ライフを。