はじめに
Record<string, any>
とRecord<string, unknown>
は同じようで、実は違う挙動をする事を知った。その経緯を含めて、挙動の違いに関しての整理を自身の備忘録として残しておく。
実際に違いを確認してみる
以下のような型定義をしてみて、それぞれの違いを確認してみる。
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>
は、それぞれ以下のようなインデックス型の事。
// Record<string, any>
{
[P in string]: any;
};
// Record<string, unknown>
{
[P in string]: unknown;
};
Recordの実装は以下。
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に詳細が書かれているのでそちらを参照。
本題のどのような変更が入ったかだが、インデックスシグネチャ{ [s: string]: any }
は特別な振る舞い(上記で見たようにkey-valueのオブジェクト型でなくてもなんでもOKになる)をするが、それが削除されたというもの。したがって、なんでもかんでもが許可される型ではなく、key-valueのオブジェクトの型のみが{ [s: string]: unkonw }
(Record<string, unknown>
)では許可されるようになった。
この辺りの詳細は{ [k: string]: unknown } is no longer a wildcard assignment targetに書かれている。
※{ [s: string]: any }
が特別な振る舞いになる根拠については調べてもあまり分からなかったが、多分こういう事だろうと思っている事としては以下。
- JavaScriptにおいてはプリミティブなもの以外は全部オブジェクトであり、そのオブジェクトには必ず存在するプロパティがある
- そのプロパティは
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
- 参考:{ [k: string]: unknown } is no longer a wildcard assignment target
- 参考:TypescriptでRecordを引数にとる関数にinterfaceで型つけしたオブジェクトを渡せないのはなぜでしょうか。
おまけ
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ならば{[x: string]: string}です。
interfaceはインデックスシグネチャ型に割り当てることはできませんが、type aliasは割り当てることができます。type aliasは型安全をいくらか犠牲にして暗黙的にインデックスシグネチャ型を推論することで、利便性を提供しているのだと考えています。
これは設計上意図してそうなっており、長い間議論されてます。
https://github.com/microsoft/TypeScript/issues/15300