Array.prototype.keyMap

I've noticed a common pattern that I think could stand to be simplified.

  const labels = {
    type1: 'Type 1',
    type2: 'Type 2',
    type3: 'Type 3',
  };

  const descriptions = {
    type1: 'Type 1 Description',
    type2: 'Type 2 Description',
    type3: 'Type 3 Description',
  };
  
  // assume that this `type` variable updates reactively
  let type = 'type3';
  const [label, description] = [labels, descriptions].map(obj => obj[type]);

I found myself doing this quite a bit, where I had to access a certain key from each of the objects in an array. What if we had something like keyMap(key) method for arrays, that allowed you to get the value for that key in several objects at once? It's not hard as it is, but it would certainly streanline the process a bit. For example:

const [label, description] = [labels, descriptions].keyMap(type);

// or

[[1, 2, 3], [4, 5, 6], [7, 8, 9]].keyMap(1) // [2, 5, 8]

The implementation could be something like this:

Array.prototype.keyMap = function(key) {
  return this.map(obj => obj[key]);
}

So with this, you'd get either the value at the key, or undefined if the key isn't there.

For your particular case,

it would be simpler and shorter to write

const label = labels[type], description = descriptions[type];

For the general case, an array method is far too specifc. For better composability, this should be an Object method to create a property accessor function:

[1, 2, 3], [4, 5, 6], [7, 8, 9]].map(Object.get(1)) // [2, 5, 8]

with

Object.get = key => obj => obj[key];
1 Like

@bergus An Object.prototytpe method like that might be confusing to users, as I would generally assume an Object.get() method to do something like this:

Object.get({ a: 1 }, 'a') // 1

That said, if that's not a valid a concern, I think your idea of .map(Object.get(key)) would work extremely well for this, except that it's actually longer-form.

[[1, 2, 3], [4, 5, 6]].map(Object.get(1))
[[1, 2, 3], [4, 5, 6]].map(o => o[1])

What of we used a new syntax (and I don't think this is valid syntax currently, so hopefully no conflicts):

let key;

key = 'a'
[{ a: 1, b: 2 }, { a: 3, b: 4 }].map([type]) // [1, 3]

key = 'b'
[{ a: 1, b: 2 }, { a: 3, b: 4 }].map([type]) // [2, 4]

[{ test1: 1, test2: 2 }, { test3: 3, test4: 4 }].map(['test2']) // [2, undefined]

[[1, 2, 3], [4, 5, 6]].map([1]) // [2, 5]

Both of these appear to produce an error currently, as you'd expected. This would introduce new support for destructuring a key directly as the map's return value, instead of passing a callback.

This way, no new method would be required, instead building upon the usage of Array.prototype.map

This same new syntax could have implications for other array methods as well and be beneficial in their cases as well.

For example, in the case of Array.prototype.filter, you might typically write something like this:

const stores = [
  { id: 1, city: 'Test 1', open: false },
  { id: 2, city: 'Test 2', open: true }
];

const openStores = stores.filter(store => store.open)
// or
const openStores = stores.filter({ open } => open)

With this new method, you could check the key without needing to use a redundant parameter like this, while retaining the same level of type safety and narrowing:

const openStores = stores.filter(['open'])

This would be equally helpful for use with other methods like find, findLast, findIndex, findLastIndex, some, every, etc.), an example might look like this. For example:

const stores = [
  { id: 1, city: 'Test 1', closed: false },
  { id: 2, city: 'Test 2', closed: true }
];
const someStoresOpen = stores.some(['open']);
const noStoresOpen = !stores.some(['open']);
const allStoresOpen = stores.every(['open']);
const notAllStoresOpen = !stores.every(['open']);

Given that [[1, 2, 3], [4, 5, 6]].map(o => o[1]) is not hard to get right, slow, unclear, inconvenient, or a common need, why would it make sense to add anything to the language for this, especially syntax (the most expensive thing)?

1 Like

@ljharb reduce isn't too hard to get right, though I believe there was still benefit in adding map:

[[1, 2, 3], [4, 5, 6]].reduce((a, x) => [...a, x[1]], [])
[[1, 2, 3], [4, 5, 6]].map(x => x[1])
[[1, 2, 3], [4, 5, 6]].keyMap(1)

I agree that new syntax is expensive, and honestly, I think the new syntax I showed above would be more confusing than not.

I do believe a new method would be simpler, and would bring a quality of life benefit to the dev community as I and many other devs use this pattern a ton

Our codebases begin to feel like this:

[user0, user1, user2].map((user) => user.name)
[data0, data1, data2].map((data) => obj.created_at)
[event0, event1, event2].map((event) => obj.datetime)
// and so on, often several times in the same module

As you said, the existing implementation using Array.prototype.map is not bad, though I think this would be a very welcome change and improvement in cases like this.

The above examples would become:

[user0, user1, user2].keyMap('name')
[data0, data1, data2].keyMap('created_at')
[event0, event1, event2].keyMap('datetime')

If we were to introduce new syntax, I would probably disregard my earlier proposal of the ([…]) syntax, as I think that would cause tons of confusion, and in that case, Array.prototype.map would simply accept valid key types (e.g. string, number, symbol), in which case those would become:

[user0, user1, user2].map('name')
[data0, data1, data2].map('created_at')
[event0, event1, event2].map('datetime')

Empirically i think reduce is exceedingly hard to get right :-)

3 Likes