.map() equivalent for objects - Object.mapEntries()

I'm proposing we add the method Object.mapEntries() into the language, to allow one to map one object to another object. This is a very common task, and it would make a lot of code just a little more readable if there was a canonical way to do so.

It could be defined as follows:

Object.mapEntries = (obj, mapFn) => {
  const entries = Object.entries(obj)
  return Object.fromEntries(entries.map(mapFn))
}

Example usage:

const users = {
  NKpiZ: { name: 'Sarah', age: 5 },
  UaAFu: { name: 'Samuel', age: 10 },
  avTRz: { name: 'Samantha', age: 15 },
}

const userIdToAge = Object.mapEntries(users, ([id, { age }]) => [id, age])
// userIdToAge is { NKpiZ: 5, UaAFu: 10, avTRz: 15 }
const nameToAge = Object.mapEntries(users, ([, { name, age }]) => [name, age])
// nameToAge is { Sarah: 5, Samuel: 10, Samantha: 15 }
const nameToId = Object.mapEntries(users, ([id, { name, age }]) => [name, id])
// nameToId is { Sarah: 'NKpiZ', Samuel: 'UaAFu', Samantha: 'avTRz' }

We could potentially add Object.mapKeys() and Object.mapValues() too, but mapEntries() alone would be a big win.

Would be nice alternative to the ‘reduce pattern’ I’ve seen a bit.

toEntries(obj).reduce((out, ([key, value]) => {
  out[key] = change(value);
  return out;
}, {})

FromEntries is also another case where I’ve wanted the pipeline operator:

toEntries(obj)
  .filter(([key]) => condition(key))
  .map(([key, value]) => [key, change(value)])
  .filter(([_, value]) => condition(value))
  |> fromEntries;

See the comments at agoric-sdk/objArrayConversion.js at 4890dcdbee725d15c1656f3882c6d49aa030778e · Agoric/agoric-sdk · GitHub

The comments are on essentially the same code. Once I started explaining it well enough for it to be used reliably, I was surprised at how much I needed to say.

I purposely chose to do entries.map(entry => mapFn(entry)) rather than entries.map(mapFn) because in the latter, the binding of other parameters of mapFn beyond the first doesn't make any sense as part of the meaning of mapEntries.

I support this abstraction. In writing that explanation, I came to appreciate all the possible variations and ways to get this wrong. So standardizing on this one would be good.

1 Like

Good point @markm, some of those other parameters wouldn't be needed by the mapping function. Certainly, an index shouldn't be provided (yes, object ordering is stable, but we probably shouldn't encourage code to depend on the order of the object's entries). However, it might be good to pass in the original object as a second parameter? Same way array.map() passes in the original array as the third parameter? I've personally had never found that to be a useful parameter, but I assume others have.

Object.mapEntries = (obj, mapFn) => {
  const entries = Object.entries(obj)
  return Object.fromEntries(entries.map(entry => mapFn(entry, obj)))
}

Since we don't have a good analogy for the second parameter and would therefore omit it, we no longer have a least-surprise reason for the original-object parameter. Other than least-surprise by symmetry, I do not find any other reason for this extra parameter compelling. We should omit it.

Sounds good to me - unless someone can think of a compelling use case for the original object reference parameter, it makes sense to just leave it out.

Feedback seems to be generally positive, so I went ahead and threw together a proposal repo here to help aid further discussion.

As I was researching how other languages and libraries handled this issue, I found that most of them did so by only providing a mapValues() function, or both mapValues() and mapKeys(). None of the languages/libraries I looked at allowed modifying both the key and value at the same time.

I still think Object.mapEntries() would be an intuitive and powerful tool in the javascript ecosystem, especially since we already have a notion of what an "entry" means, because of functions such as Object.entries() or Object.fromEntries(). But, there's certainly room for discussion here on whether we want to pursue the original idea of having Object.mapEntries(), or if we should have an implementation similar to other languages/libraries and have both Object.mapKeys() and Object.mapValues(), or do some combination of both. (I also outlines these thoughts in the github repo, under the "Comparison" heading).

Another alternative could be just “map” to avoid asks for keys/values - or, map with an optional second argument that defaults to “entries” but can also be “keys” or “values”.

When you say

map with an optional second argument that defaults to “entries” but can also be “keys” or “values”.

Are you referring to using a literal string as a second argument to control the behavior of the mapping function? i.e. Object.map(obj, entries => newEntries) and Object.map(obj, 'keys', key => newKey). I don't think any ECMAScript API uses string sentinels like that.

If we do choose to just have a single Object.mapValues() function, I think it would make sense to just give it your suggested name of Object.map() instead.

This has been proposed before and was rejected by the commitee. Please see GitHub - tc39/proposal-object-iteration: ECMA TC39 proposal for making mapping over Objects more concise and its issues for existing discussion.

Ah right, third argument then :-)

Ah man, I guess this is a lost cause then :(

I don't think they'll ever get people to start using the Map() in place of normal objects all the time - there's so much syntax support around normal objects that won't ever exist with Map(). (not that Map() isn't useless, it's plenty useful, it's just more verbose to use).

I do see the concern about it being a slippery slope (assuming they're talking about a slippery slope of adding other functions such as Object.filter()) - I was a little worried about the same thing.

I don't see how Object.mapEntries(obj, fn) is any better than Object.fromEntries(Object.entries(obj).map(fn)) except for a mild perf boost.

I do see the value in Object.mapValues(obj, fn), though - engines could make it as fast as Array.prototype.map if not faster as they can just reuse the existing object's type info.