0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-06-28

はじめに

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.

参考

おまけ

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

Record型はインデックスシグネチャの型を作ります。例えばRecordならば{[x: string]: string}です。

interfaceはインデックスシグネチャ型に割り当てることはできませんが、type aliasは割り当てることができます。type aliasは型安全をいくらか犠牲にして暗黙的にインデックスシグネチャ型を推論することで、利便性を提供しているのだと考えています。

これは設計上意図してそうなっており、長い間議論されてます。
https://github.com/microsoft/TypeScript/issues/15300

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0