Objecty Arrays in JavaScript

Can we have our cake and eat it too?

The ideas in this post are implemented in the keyed-array package on npm.

A question often comes up for me when I'm designing a JavaScript or JSON data structure: "Should I use an Array or an Object for this?" Sometimes the answer is obvious. Sometimes it's hard to know which is better.

This short post proposes One Weird Trick™ for adding a bit of sugar to Arrays, using plain old JavaScript.

Imagine you're building a JSON webservice that will return a collection of people. Each person is an object:

{
  id: 'bob',
  name: 'Bob',
  isCool: false
}

You could structure the JSON response as an array:

people = [
  {id: 'bob', name: 'Bob', isCool: false},
  {id: 'sue', name: 'Sue', isCool: false},
  {id: 'hal', name: 'Hal', isCool: true},
  {id: 'ann', name: 'Ann', isCool: true}
]

This is nice because Arrays have lots of methods for dealing with collections, like forEach, map, and sort. Gathering up the names of all the cool people is pretty easy:

people
  .filter(person => person.isCool)
  .map(person => person.name)

But when you want a person with a specific id, you have to do something like this:

people.find(person => person.id === 'bob')

Kind of cumbersome. A simple people.bob would be more convenient.

Instead of storing the id in the object, use it as a key:

people = {
  bob: {name: 'Bob', isCool: false},
  sue: {name: 'Sue', isCool: false},
  hal: {name: 'Hal', isCool: true},
  ann: {name: 'Ann', isCool: true}
}

The nice thing about this structure is that you can easily access a person if you know their id:

people.bob

The drawback of this structure is that it's more cumbersome to iterate over an Object than an Array. The code for fetching the names of cool people now looks like this:

Object.keys(people)
  .filter(id => people[id].isCool)
  .map(id => people[id].name)

Not so great.

Here's the crazy idea: Use an Array of person objects, but iterate over it once at load time, assigning a key for each person's id:

people.forEach(person => people[person.id] = person)

This way it's still an array:

Array.isArray(people) // => true 
people[0].name        // => Bob 
people.length         // => 4 

But there's now a named key for each person, so content is now addressable by id:

people.bob.isCool     // => true 
('sue' in people)     // => true 

This is kind of the best of both worlds, but there are a few drawbacks. Object.keys(people).length is no longer reliable, as it returns both the numeric keys of the array and the named keys we assigned:

Object.keys(people)
// => ['0', '1', '2', '3', 'bob', 'sue', 'hal', 'ann'] 

Also JSON.stringify will produce undesirable results. But if those two drawbacks are not important for your use case, this little technique could fit your needs.

If you found this useful, let me know. If you think it's a bad idea, tell me why.