Home›Journal›This post

Object.groupBy vs Map.groupBy: Pick the Right Key

Run mixed primitive, symbol, identity, ordering, and serialization fixtures before choosing Object.groupBy or Map.groupBy for product data.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

Object.groupBy vs Map.groupBy is not a style preference: the choice changes which keys collide, how groups are retrieved, and what survives serialization. This guide uses adversarial fixtures to turn “object or map?” into a small contract about property keys, identity, order, and handoff format.

Object.groupBy vs Map.groupBy starts with the key

Object.groupBy vs Map.groupBy begins with the classifier's actual return domain. Both APIs perform JavaScript grouping over the same elements and collect references into arrays, but they disagree about what constitutes a group key. Choosing by preferred syntax can merge categories that looked distinct or create identity groups that cannot cross a JSON boundary.

Ask four questions before writing the callback. Can a key be a symbol? Can it be an object whose identity matters? Can numbers and strings both appear? Must the output become plain JSON? Those answers usually select the container without a benchmark.

The comparison is narrower than general data transformation. Iterator helpers for data pipelines cover how values reach a grouping step. This guide owns the output contract after classification: property-key coercion, arbitrary-key identity, enumeration order, shared element references, capability detection, and serialization adaptation.

The lab uses adversarial fixtures instead of friendly department names. It places number 1 beside string "1", boolean true beside string "true", stable object identities beside structurally equal fresh objects, two symbols with the same description, NaN, signed zero, the string proto, and integer-like keys. The receipt shows exactly which members merged, how keys are retrieved, and what naive JSON would lose.

One classifier two key pipelinesThe same callback result enters property-key coercion for Object.groupBy or identity and SameValueZero matching for Map.groupBy.SAME ELEMENTS · DIFFERENT KEY CONTRACTclassifier(row)1 · “1” · {k:1}Object.groupBy · ToPropertyKey1 + “1” merge · objects stringifyMap.groupBy · identity / SameValueZero1 ≠ “1” · objects stay by identityarrays are new · member objects are shared references
Object output coerces property keys, while Map output preserves primitive type and object identity.
Key pipeline results
Classifier resultObject.groupBy keyMap.groupBy keyOutcome
number 1string “1”number 1Object merges with string
string “1”string “1”string “1”Map keeps separate
object Astring “[object Object]”object identity AObject may collide
symbol Asymbol Asymbol Aboth preserve symbol identity

Reading rule: labels, markers, and the table carry every conclusion; color is supplementary.

Object.groupBy converts keys to property keys

Object.groupBy applies the specification's property key coercion to each classifier result. Strings remain strings. Symbols remain symbols. Numbers, booleans, BigInts, null, undefined, and objects become strings through the relevant coercion path. Number 1 and string "1" therefore share a group; boolean true and string "true" share another.

The result is a null-prototype object. That prevents inherited Object.prototype properties from appearing as groups and makes proto an ordinary own property rather than a prototype mutator. It does not sanitize grouped elements or classifier output. Access patterns must use own-property operations, bracket lookup, and explicit symbol handling instead of assuming methods such as hasOwnProperty exist on the result.

Integer-index-like string keys have special enumeration ordering. A callback may first produce "10", then "2", then "alpha", yet Object.keys can enumerate "2", "10", "alpha" because property order treats array-index keys separately. If first-seen group order is a product requirement, object output may need an explicit key-order sidecar or a Map.

The ECMA-262 Object.groupBy algorithm is the authority for coercion and the null-prototype result. Object.groupBy vs Map.groupBy should therefore be documented in terms of normative key semantics, not an assumption that “objects are simpler.”

Map.groupBy preserves arbitrary key identity

Map.groupBy keeps arbitrary classifier values as Map keys, including object identity keys. Objects group by identity, not by structure. Returning the same category object for several rows creates one group; returning a new object with the same tier value for every row creates separate groups even though the objects serialize alike. This is useful when category objects carry live configuration, but surprising when the callback rebuilds labels.

Primitive equality uses SameValueZero. NaN groups with NaN. Positive and negative zero share a key. Strings remain distinct from numbers and booleans. Symbols remain identity keys, so two symbols with the same description do not merge. The lab assigns stable display IDs to object and symbol keys because stringifying them would erase the behavior being audited.

Maps preserve insertion order for keys. The first time a classifier result appears fixes its group position, including integer-like strings. Retrieval uses map.get with the original identity. Reconstructing an equivalent-looking object will not retrieve an existing identity group.

The ECMA-262 Map.groupBy algorithm defines this grouping behavior. Object.groupBy vs Map.groupBy is therefore not “object for speed, Map for flexibility.” The useful distinction is whether the key space is property-like or identity-bearing and whether first-seen order must remain directly iterable.

Run the collision fixtures before choosing

A collision cabinet turns abstract rules into a product decision. Feed rows whose classifiers return 1, "1", true, "true", null, "null", NaN, two zeros, a shared object, two fresh lookalikes, a shared symbol, and a second same-description symbol. For each output, list the type-tagged key, member row IDs, first-seen position, and serialization status.

Object output intentionally merges the number/string and boolean/string pairs. It stringifies NaN, null, and objects, so unrelated objects may collide as "[object Object]" unless the callback returns an explicit property key. Symbols remain separate but disappear from Object.keys and ordinary JSON serialization unless handled explicitly.

Map output keeps primitive types separate, merges NaN under SameValueZero, merges signed zero, retains object identity, and keeps symbol identities. That precision can be the desired model or an accidental explosion of groups. A fresh object callback is usually a bug when the product meant structural categories. Unicode for product interfaces is a reminder that labels which look alike may also differ at the string level; grouping does not normalize them for you.

Figure 2 repeats the matrix with exact merge/separate outcomes and ordering notes. Object.groupBy vs Map.groupBy becomes easy to review when a teammate can point to a concrete collision that the chosen container either requires or forbids. Friendly fixtures cannot supply that evidence because they make both APIs look equivalent.

Adversarial collision cabinetDrawers show which mixed keys merge separate reorder or disappear from naive serialization.OPEN EVERY DRAWER BEFORE CHOOSING THE CONTAINER1 + “1”Object mergesMap separatestrue + “true”Object mergesMap separatesNaN + NaNboth mergeSameValueZeroobject twinsObject mergesMap separates10 then 2Object reordersMap first-seensymbol twinsboth separateJSON omits
Mixed primitive, object, ordering, and symbol fixtures reveal the semantic differences hidden by friendly string keys.
Collision and handoff matrix
FixtureObject resultMap resultBoundary note
1 / “1”one grouptwo groupscoercion collision
true / “true”one grouptwo groupscoercion collision
NaN / NaNone groupone groupSameValueZero
fresh lookalike objectsone string groupseparate identitiesneeds stable IDs
“10” then “2”enumerates 2 then 10keeps 10 then 2order differs
same-description symbolsseparate symbol keysseparate symbol keysnaive JSON omits

Reading rule: labels, markers, and the table carry every conclusion; color is supplementary.

Decide how groups cross a boundary

Object results fit property-oriented handoff more naturally, but symbols still require an adapter and dangerous-looking names still deserve explicit policies. JSON.stringify ignores symbol-keyed properties. It also serializes grouped values according to their own JSON behavior; a null prototype offers no data-safety guarantee.

Map results do not serialize with JSON.stringify: the naive output is an empty object. Converting with Object.fromEntries can reintroduce string coercion and collisions, destroying the reason a Map was selected. A loss-aware adapter must encode each key with a type tag and stable identity ID, then emit ordered entries and member IDs. Object and symbol identities cannot be reconstructed across processes unless the application defines a separate durable identifier.

Structured clone versus JSON compares wider browser-state boundaries. Here, the decision is smaller: whether grouping output stays inside one live JavaScript graph or crosses into logs, caches, workers, APIs, or documents. Every crossing needs a named adapter and a loss statement.

Object.groupBy vs Map.groupBy should be selected with the boundary in the same design review as the callback. If plain JSON is the required product artifact and keys are already safe strings, Object output is direct. If identity keys are essential inside a session, Map is honest, but the durable handoff must carry explicit IDs rather than pretending JSON preserved identity.

Remember that values are shared references

Neither grouping API clones elements. Each group's array contains references to the original objects. Mutating a grouped row mutates the source row, and mutating the source is visible through every group that holds it. The grouping arrays are new containers, but their members are not new values.

That behavior can be efficient for read-only views and dangerous in editing workflows. A table that edits a grouped customer record may also change the master list before a save boundary. A test that only compares serialized snapshots can miss aliasing if mutation happens later.

The current MDN Map.groupBy reference highlights identity-key use and the fact that grouped elements remain the same objects. The lab proves this with a frozen fixture: update one source label after grouping, observe the change through both outputs, then restore the fixture before hashing. The receipt records that members are shared references rather than presenting the groups as copies.

TypeScript's role at runtime boundaries is relevant because static key types cannot enforce safe coercion, identity durability, or JSON adaptation at runtime. Object.groupBy vs Map.groupBy still needs validation and an explicit mutation policy even in a well-typed codebase.

Grouping boundary handoffA decision map sends property keys directly toward JSON while identity keys require a type-tagged loss-aware adapter.CHOOSE THE ADAPTER WITH THE KEY DOMAINclassifier domainproperty or identity?PROPERTY KEYSnull-prototype objectadapt symbols explicitlyIDENTITY KEYSordered Map entriestype tag + stable durable IDJSON.stringify(Map) = {} · Object.fromEntries may lose semantics
Property-key output can cross a named-property boundary directly; identity-key output needs explicit type tags and durable IDs.
  1. If keys are safe strings and JSON is the product artifact, choose Object.groupBy and adapt symbols explicitly.
  2. If object identity is meaningful inside one live graph, choose Map.groupBy.
  3. Before a Map crosses a process boundary, encode ordered entries with type tags and stable durable IDs.
  4. Never label JSON.stringify(Map) as successful serialization; it produces an empty object.
  5. Do not use Object.fromEntries when coercion would destroy the selected identity semantics.

Ship a capability-detected contract

Modern runtimes increasingly expose both APIs, but a durable tutorial should not make native availability the correctness oracle. The lab detects Object.groupBy and Map.groupBy, runs them when present, and compares their type-tagged output with small reference implementations derived from the specification. When a capability is missing, the reference path remains clearly labeled rather than mutating globals with a surprise polyfill.

The classifier comes from a bounded catalog: mixed primitives, stable identity, recreated identity, symbols, integer-like order, and a declared field name for validated JSON rows. No arbitrary callback text is evaluated. Input is limited to 256 rows, unknown keys are rejected, and output is constructed with text nodes. There is no network, storage, or dynamic code.

Tests cover empty input, coercion collisions, NaN, signed zero, symbol identity, stable and recreated objects, proto, integer ordering, null prototype, shared references, native absence, malformed or over-cap rows, and deterministic serialization. Mutants fail missing property-key coercion, normal prototypes, structural Map equality, split NaN, split zero, stringified symbols, and unmarked naive Map JSON.

Avoid universal performance claims. Allocation, engine version, key distribution, access pattern, and serialization can dominate. Object.groupBy vs Map.groupBy is first a semantic choice. Benchmark only after both candidates can represent the required key and boundary contract without lossy repair.

Publish the grouping decision table

The final decision table needs the classifier domain, output type, collision policy, retrieval form, ordering rule, prototype status, shared-reference policy, serialization adapter, capability path, and known losses. Include one adversarial fixture that would fail under the rejected API so the choice remains legible after refactoring.

Choose Object.groupBy when classifiers intentionally produce property keys, null-prototype output is useful, and the boundary expects named properties. Choose Map.groupBy when arbitrary keys or object identity are part of the model and the result stays in a live graph or has an explicit type-tagged adapter. Do not choose Map merely to avoid thinking about coercion, and do not choose Object merely because JSON exists.

Revisit this Object.groupBy vs Map.groupBy guide on 2027-02-16, or sooner if ECMA-262 algorithms change, browser baseline or TypeScript declarations move, search queries reveal identity or serialization confusion, a fixture diverges, or a source breaks. Refresh the comparison, lab capability notes, and receipt together.

The right API is the one whose key semantics already match the product. A compact collision receipt makes that judgment testable before real data discovers it for you.

Runnable local artifact — The lab compares key semantics and handoff loss; it does not clone grouped members, sanitize values, preserve identity across JSON, or claim a universal performance winner.

Plain text1 line
Run a bounded classifier catalog over at most 256 rows, compare native and reference groupings, expose collisions and shared references, and export a type-tagged deterministic receipt.