Object.groupBy で作られるオブジェクトの prototype は null

おさらい: prototype

JavaScript のオブジェクトはみんな prototype というのを持っていて, この prototype からプロパティを継承, より正確には, プロパティアクセス時にそのプロパティがオブジェクトに存在しなければ prototype を辿って見つけにいくことになっている.

あるオブジェクトを prototype とした別のオブジェクトを作るには Object.create を使う (あるいは new 演算子__proto__ を使っても良い).

const x = {};
x.foo = "foo";

const y = Object.create(x);
y.bar = "bar";

const z = Object.create(y);
z.baz = "baz";

console.log(z.foo); // => "foo"
console.log(z.bar); // => "bar"
console.log(z.baz); // => "baz"

逆に, あるオブジェクトの prototype を取得するには Object.getPrototypeOf を使う.

console.log(Object.getPrototypeOf(z) === y); // => true

ところで {} のような「空」のオブジェクトにも prototype が存在して, これは Object.prototype と呼ばれる. この Object.prototype には toString のようなプロパティが定義されているので, 空のオブジェクトは真に空というわけではない.

const x = {};
console.log(Object.getPrototypeOf(x) === Object.prototype); // => true
console.log(x.toString); // => [Function: toString]

では真に空のオブジェクトは作れないのかというとそんなことはなくて, prototype が null のオブジェクトを作れば良い. 例えば Object.create(null) などとする (ブログタイトル回収).

const w = Object.create(null);
console.log(Object.getPrototypeOf(w)); // => null
console.log(w.toString); // => undefined

Object.groupBy

Object.groupBy は ES2024 で追加されたメソッドで, 配列などの iterable をグループ化して, それらのグループをプロパティに持ったオブジェクトを作成できる.

const arr = [3, 1, 4, 1, 5, 9, 2]
const obj = Object.groupBy(arr, (e) => e % 2 === 0 ? "even" : "odd");
console.log(obj); // => { odd: [3, 1, 1, 5, 9], even: [4, 2] }

そして記事のタイトルの通り, Object.groupBy で作られるオブジェクトの prototype は null になっている.

console.log(Object.getPrototypeOf(obj)); // => null

このメソッドを追加する proposal の README を読むと, これには prototype から継承したプロパティと Object.groupBy によって作られたプロパティが混ざって困ったことが起こらないように, という意図があるようだ.

returns a null-prototype object, which allows ergonomic destructuring and prevents accidental collisions with global Object properties

https://github.com/tc39/proposal-array-grouping?tab=readme-ov-file#motivation

TypeScript と null-prototype オブジェクト

ところで TypeScript には prototype が null のオブジェクトを表す型はない. 空のオブジェクト型 {} がそれに該当すると思われるかもしれないが, 実は全てのオブジェクト型で toString などの Object.prototype の持つプロパティは暗黙的に継承されていることになっていて, プロパティアクセスが行えてしまう.

つまり以下のようなコードは型検査を通過するが, 実行してみるとエラーが発生する.

const obj = Object.groupBy([], () => "*");
obj.toString(); // => TypeError: w.toString is not a function

これまで prototype が null のオブジェクトが登場するような状況は (私の知る限り) かなり限られていたのであまり気にならなかったのだが, 今後は Object.groupBy の普及に伴ってしばしば登場するかもしれないので注意が必要そうである.

そもそもオブジェクトを辞書のように用いる (index signature を使う) のはいくらか型安全性に問題があるため, 多くの場合で Object.groupBy よりも Map.groupBy を使う方が適しているだろうということは覚えておきたい. また誤ったメソッドの呼び出しがもしあれば気がつけるように, ESLint の no-prototype-builtinsno-base-to-string といったルールも有効化しておくと良い.