TypeScriptのRecord<string, any>とRecord<string, unknown>の振る舞い・挙動の違い

## はじめに

`Record<string, any>`と`Record<string, unknown>`は同じようで、実は違う挙動をする事を知った。その経緯を含めて、挙動の違いに関しての整理を自身の備忘録として残しておく。

## 実際に違いを確認してみる

以下のような型定義をしてみて、それぞれの違いを確認してみる。

```ts
type RecordAny<T> = T extends Record<string, any> ? 'true' : 'false';

type T1 = RecordAny<() => 123>; // type `T1` is 'true'
type T2 = RecordAny<[]>; // type `T2` is 'true'
type T3 = RecordAny<Date>; // type `T3` is 'true'
type T4 = RecordAny<'string'>; // type `T3` is 'false'
type T5 = RecordAny<{ hoge: 'hoge' }>; // type `T3` is 'true'

type RecordUnkonw<T> = T extends Record<string, unknown> ? 'true' : 'false';

type P1 = RecordUnkonw<() => 123>; // type `P1` is 'false'
type P2 = RecordUnkonw<[]>; // type `P2` is 'false'
type P3 = RecordUnkonw<Date>; // type `P3` is 'false'
type P4 = RecordAny<'string'>; // type `P4` is 'false'
type P5 = RecordUnkonw<{ hoge: 'hoge' }>; // type `P5` is 'true'
```

上記のように、`Record<string, any>`の場合はそれこそオブジェクトであればなんでも継承するので、`'string'`以外にの全てにおいてリテラル型の`'true'`になる。`Record<string, unknown>`の場合は、key-valueのものだけがリテラル型の`'true'`になる。

※`Record<string, any>`と`Record<string, unknown>`は、それぞれ以下のような[インデックス型](https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures)の事。

```ts
// Record<string, any>
{
  [P in string]: any;
};

// Record<string, unknown>
{
  [P in string]: unknown;
};
```

[Record](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type)の実装は以下。
```ts
type Record<K extends keyof any, T> = {
  [P in K]: T;
};
```

## それぞれの違いは何か？

元々は`Record<string, any>`と`Record<string, unknown>`は全く同じものであった（TypeScript3.4までは）。しかし、TypeScript3.5でGeneric（ジェネリック）における暗黙的な型類推が`{}`から`unkonwn`に変わった事で、このanyとunknownとで挙動に変化が起きた。

ジェネリックにおける暗黙的な型類推の変更については[Generic type parameters are implicitly constrained to unknown](https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#generic-type-parameters-are-implicitly-constrained-to-unknown)に詳細が書かれているのでそちらを参照。

本題のどのような変更が入ったかだが、インデックスシグネチャ`{ [s: string]: any }`は特別な振る舞い（上記で見たようにkey-valueのオブジェクト型でなくてもなんでもOKになる）をするが、**それが削除された**というもの。したがって、なんでもかんでもが許可される型ではなく、key-valueのオブジェクトの型のみが`{ [s: string]: unkonw }`（`Record<string, unknown>`）では許可されるようになった。

この辺りの詳細は[{ [k: string]: unknown } is no longer a wildcard assignment target](https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#-k-string-unknown--is-no-longer-a-wildcard-assignment-target)に書かれている。

※`{ [s: string]: any }`が特別な振る舞いになる根拠については調べてもあまり分からなかったが、多分こういう事だろうと思っている事としては以下。

- JavaScriptにおいてはプリミティブなもの以外は全部オブジェクトであり、そのオブジェクトには[必ず存在するプロパティがある](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object)
- そのプロパティは`Object.〇〇`でアクセスできるので、プロパティもkey-valueの一種
- プロパティのvalueが`any`のサブタイプか？という意味では、Yesになるので、`{ [s: string]: any }`はオブジェクトであれば何でもかんでもOKな型になる
 
上記は、以下の記述からの仮説（誤り等あればご指摘頂けると幸いです）。

> In general this rule makes sense; the implied constraint of "all its properties are some subtype of unknown" is trivially true of any object type. 

## 参考

- 参考：[Generic type parameters are implicitly constrained to unknown](https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#generic-type-parameters-are-implicitly-constrained-to-unknown)
- 参考：[{ [k: string]: unknown } is no longer a wildcard assignment target](https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#-k-string-unknown--is-no-longer-a-wildcard-assignment-target)
- 参考：[TypescriptでRecord<string, unknown>を引数にとる関数にinterfaceで型つけしたオブジェクトを渡せないのはなぜでしょうか。](https://teratail.com/questions/326164)

## おまけ

### interfaceとtypeでの`Record<string, unknown>`（インデクスシグネチャ）における挙動の違い

- interface
インデクスシグネチャには割り当て不可能（コンパイルエラー`Index signature for type 'string' is missing in type ...`になる）
https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgYQIYgKYBs0GcMDSGAnrnAL5wBmUEIcA5AMbrYv4C0A1ibgwFCDgAOxgYoVVEwxwAClikYyCfnDgxQGAPoAvCMIwAuOLhhQRAczgAfOMICuWLAG5+5QTGJgZAJSWP4AF4UVhxUfCJSAB55RVwAPn4gA

- type
インデクスシグネチャに割り当て可能
https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgYQIYgKYBs0GcMDSGAnrnAL5wBmUEIcA5AMbrYv4C0A1ibgwFCCYxMBjgAFLKiYYyAXkT84cGKAwB9AF4QAdhgBccXDCjAdAczgAfODoCuWLAG5+5ISLEAlWQ-gK0mDio+ESkADyS0rIAfPxAA

> Record型はインデックスシグネチャの型を作ります。例えばRecord<string, string>ならば{[x: string]: string}です。  
> 
> interfaceはインデックスシグネチャ型に割り当てることはできませんが、type aliasは割り当てることができます。type aliasは型安全をいくらか犠牲にして暗黙的にインデックスシグネチャ型を推論することで、利便性を提供しているのだと考えています。
> 
> これは設計上意図してそうなっており、長い間議論されてます。
https://github.com/microsoft/TypeScript/issues/15300

- 参考：[TypescriptでRecord<string, unknown>を引数にとる関数にinterfaceで型つけしたオブジェクトを渡せないのはなぜでしょうか。](https://teratail.com/questions/326164)