環境
- typescript:
5.1.3
Object.keys()
が string[]
になってしまうのでブランケットアクセス (obj[key]
) で TypeError
const idols = { ichigo: { unit: 'Soleil', type: 'cute' }, aoi: { unit: 'Soleil', type: 'cool' }, ran: { unit: 'Soleil', type: 'sexy' }, yurika: { unit: 'Tristar', type: 'cool' }, otome: { unit: 'Powapowa Puririn', type: 'pop' }, akari: { unit: 'Luminus', type: 'cute' }, }; Object.keys(idols).map((name) => { return `${name}(${idols[name].unit})`; });
idols[name].unit
部分でTypeError が発生する
=> Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ ichigo: { unit: string; type: string; }; aoi: ...; }'.
No index signature with a parameter of type 'string' was found on type '{ ichigo: { unit: string; type: string; }; aoi: ...; }'.
Unsafe member access .unit on an
anyvalue. (eslint@typescript-eslint/no-unsafe-member-access)
Object.keys(idols)
が string[]
と推論されるためブランケットアクセスしている idols[name]
で TypeError が発生し any 型と推論される続く .unit
が Unsafe member access
になってしまっている
Object.keys()
の型をいい感じにする
as
(Type Assertion) を使う- ラッパー関数を作成する
ObjectKeys
の型定義を上書きする
1. 返り値に as
(Type Assertion) で型定義してしまう
Object.keys(idols).map((name) => {})
のname
が string 型として推論されている為に TypeError が発生しているname
がidols
オブジェクトのいずれかのキーを表す型であれば問題ないidols
のキーの Union 型はkeyof typeof idols
- ブランケットアクセスしている
name
をas
でkeyof typeof idols
型だと解釈させればよい
Object.keys(idols).map((name) => { return `${name}(${idols[name as keyof typeof idols].unit})`; });
シンプルな方法だが、name を使う箇所が増えるたびに Type Assertion を書く必要があり keyof typeof <OBJECT>
で指定する変数を間違うと型エラーを握り潰してエラーがあるコードがデプロイされてしまう可能性もある
2. ラッパー関数を作成する
const objectKeys = <T extends { [key: string]: unknown }>( obj: T, ): (keyof T)[] => { const res = Object.keys(obj); return res; // => res は string[] 型だが (keyof T)[] として扱われる }; objectKeys(idols).map((name) => { return `${name}(${idols[name].unit})`; // name は `name: "ichigo" | "aoi" | "ran" | "yurika" | "otome" | "akari"` 型に推論される });
cf. 【TypeScript】Object.keys() の返り値をstring[]型でなくユニオン型の配列にしたい
ラッパー関数の返り値を infer などで制限すると Type Assertion が必要になってしまった
type ObjectKeys<T> = (keyof T extends infer U ? U extends keyof T ? U : never : never)[]; const objectKeys = <T extends { [key: string]: unknown }>( obj: T, ): ObjectKeys<T> => { const res = Object.keys(obj); return res; // => Type 'string[]' is not assignable to type 'ObjectKeys<T>'. };
ラッパー関数の返り値の型を (keyof T)[]
から 制限を加えた ObjectKeys<T>
に変更すると帰り値の res
が関数の返り値の指定と異なるとして Type 'string[]' is not assignable to type 'ObjectKeys<T>'
が発生した
この場合は返り値を Type Assertion as
を使わざるを得なかった
const objectKeys = <T extends { [key: string]: unknown }>( obj: T, ): ObjectKeys<T> => { const res = Object.keys(obj); - return res; + return res as ObjectKeys<T>; };
cf. Object.keys() が string[] を返してくる問題を解決する3つの方法
3. ObjectKeys
の型定義を上書きする
interface ObjectConstructor { // … 略 /** * Returns the names of the enumerable string properties and methods of an object. * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object. */ keys(o: {}): string[]; }
Object.keys
の型は ObjectConstructor
として定義されているようで、これをプロジェクト側で上書きしてしまう方法
/src/types/ObjectKeys.d.ts
type ObjectKeys<T> = T extends { [key: string]: unknown } ? (keyof T)[] : never; interface ObjectConstructor { keys<T>(o: T): ObjectKeys<T>; }
cf. TypeScript: Improving Object.keys
※ Object.keys
に number や Date オブジェクトを渡すと []
が返ってきたり、配列や文字列を渡すと要素数や文字数の string[]
が帰ってくるが、{[key: string]: any}
なオブジェクトしか渡してほしくないので、オブジェクトが渡される前提で型定義しました
📝 number
, Symbol
, Date
, Function
=> []
, Array<any>
, string, Class => string[]
型定義については理解が浅いので若干雰囲気で書いていますが、上記 3 つの方法で Object.keys
の返す型をオブジェクトのキーの Union 型の配列にすることができました! 血直接 as keyof tyoeof Object
を使う方法はタイポやオブジェクトの指定ミスなど発生しうるのでラッパー関数を使うか ObjectConstructor
内にある Object.keys
の定義を上書きしてしまうのが良さそうに思いました。
⚡ Tips Object.keys()
が string[]
型になるのは JavaScript の仕様のため
JavaScript の Object はキーに string と number を取ることができるが、Object.keys()
は暗黙の型変換でキーを全て string として返す仕様があるので TypeScrip では Object.keys()
は string[]
型を返すものとなっているっぽい
// JavaScript はオブジェクトのキーを string と number で指定できる const obj = { 0: 'a', '0': 'b', '1': 'c', 1: 'd', 2: 'e' }; // string と number で同じ値は暗黙的に同じキーに変換される console.log(obj); // => {0: 'b', 1: 'd', 2: 'e'} // Object.keys() は暗黙的に文字列に変換する Object.keys(obj); // => ['0', '1', '2']
Object.keys()
は、object
で直接発見された列挙可能なプロパティに対応する文字列を要素とする配列を返します。
cf. Object.keys() - JavaScript | MDN
所感
Object.entries
のときも型にハマった記憶があるけど、まさか Object.keys
にもハマるとは思ってませんでした。
Object.keys
の返す型が string[]
は予期しておらず、純粋に JavaScript のオブジェクトはキーに 文字列も数値も使えるが、Object.keys
は number
のキーを暗黙の型変換で string
にして返す仕様を知らなかったのが原因だったので勉強になりました。
おわり₍ ᐢ. ̫ .ᐢ ₎
[参考]