Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

Object.keysの型をより正確にしてみる

【TypeScript】Object.keys() の返り値をstring[]型でなくユニオン型の配列にしたい こちらの記事を読んでみて、「テンプレートリテラルを使えばもっと正確にできるのでは?」と思って書いてみました

おさらい: なぜObject.keysの返り値は string[] なのか?

(上記記事で詳しく説明されているので、ここでは軽いおさらいだけ書きます)

Object.keys は、オブジェクトのキーのうちstringとnumberのみを文字列に変換して返します。例えば Object.keys({'str': 'string', 0: 'number', [symbol]: 'symbol'})['str', '0'] となります。

これに対して、keyof はオブジェクトのキーを文字列に変換せずに返します。例えば keyof {'str': 'string', 0: 'number', [symbol]: 'symbol'}'str' | 0 | typeof symbol となります。

const obj = {str: 'string', 0: 'number', [symbol]: 'symbol'};
const keys = Object.keys(obj); // ['str', '0']
type Keys = keyof typeof obj; // 'str' | 0 | typeof symbol

このように、Object.keys と keyof は挙動が異なります。

より実際に近い挙動に書き換えてみる

keyofしたものからstringとnumberを抜き出して文字列に変換すれば、実際のObject.keysの挙動と近くなるはずです。

const getKeys = <T extends Record<PropertyKey, unknown>>(obj: T) => {
    return Object.keys(obj) as `${keyof T & (string | number)}`[];
};

const s = Symbol()
const obj = { 'str': 'string', 1: 'number', [s]: 'symbol' };

const keys = getKeys(obj); // ['str', '1']
type Keys = typeof keys; // 'str' | '1'

www.typescriptlang.org

だいぶ近くはなりました。ただし、これでもまだ不完全です。

enumerable: true で定義したpropertyを取得できない

Object.definePropertyでenumerable:trueを指定してプロパティを動的に追加した場合、Object.keysにはそのプロパティが表示されます。しかし、keyofのほうでは取得できません。

Object.defineProperty(obj, 'prop', {
  enumerable:true,
  value:42,
});
Object.keys(obj); // 'prop' が含まれる
type Keys = keyof typeof obj; // 'prop' が含まれる

…まあ当たり前っちゃ当たり前なんですが、完全にはObject.keysとkeyofの対応が取れないよという話でした

まとめ: 大抵のユースケースでは十分

Object.definePropertyで動的にプロパティを足したケースに対応できていないものの、ほとんどのケースにおいては上で紹介したgetKeys関数が使えます。 コーナーケースにはまらないように気をつけつつ使っていくのが良いと思います。