TypeScriptで keyof T が string | number | symbol に推論される理由の調査
https://zenn.dev/jiftechnify/articles/2489f4103918a2 の中で問題になった、以下の挙動について理由を調べた。
-
Record<string, ...>
型の変数に数値をキーに持つオブジェクトを代入できる -
keyof T
が、T
の具体的な型が確定しない状況でstring | nunber | symbol
と推論される
結論からいうと、TypeScript 2.9で入った仕様だった。さらにいえば、このリリースノートに(上記の記事で紹介したものよりも遥かに単純な)対処法も載っていた…。
上記リリースノートから関連する部分を引用。
Changes include:
- An index type keyof T for some type T is a subtype of string | number | symbol.
Given an object type X, keyof X is resolved as follows:
- If X contains a string index signature, keyof X is a union of string, number, and the literal types representing symbol-like properties, otherwise
-
keyof T
が、T
の具体的な型が確定しない状況でstring | nunber | symbol
と推論される
これについてはそのものずばりの記述がある。
-
Record<string, T>
型の変数に数値をキーに持つオブジェクトを代入できる
これについては直接的な言及がない気がするが、string
のインデックスシグネチャを持つ場合(Record<string, T>
は{ [key: string]: T }
のエイリアスなので、これに該当)にkeyof X
がstring
だけでなくnumber
も含むunionになるという仕様から逆に考えると、意図通りの挙動だと考えられる。
JavaScriptの { 1: 'a' }
のように書くと{ '1': 'a' }
のように数値が勝手に文字列に変換される仕様と互換性を持たせるためにこうしたんだろうか? (ちょっと腑に落ちない)
時間があるときに、この仕様変更に関わるPRを読んでみる?
リリースノートで紹介されている、keyof T
の型推論結果からstring
以外を除く方法。
This is a breaking change; previously, the keyof operator and mapped types only supported string named properties. Code that assumed values typed with keyof T were always strings, will now be flagged as error.
(略)
Recommendations
- If your functions are only able to handle string named property keys, use Extract<keyof T, string> in the declaration:
function useKey<T, K extends Extract<keyof T, string>>(o: T, k: K) {
var name: string = k; // OK
}
この方法で記事の方法を置き換えられることを確認。なんでこんなに簡単な方法に気づかなかったんだろう…