Don't search an array, look up instead
4 min read
Let us imagine that I have a collection of data that needs to be searched through quite often, for example an array of users:
const users = [
{
name: 'Vinnie Coffey',
id: 345623,
},
{
name: 'Tiana Phelps',
id: 768547,
},
{
name: 'Zackary Solomon',
id: 129865,
},
{
name: 'Salma Berg',
id: 198576,
},
// -- and so on with many more objects
]
We can pretend that in real life this array of users could be quite large and
if I need to look up a user frequently it would be inefficient to continually
loop through it with something like [].find()
. The user I want could be near
the end each time!
One way to get round this is to create an object or Map
that uses a unique
identifier as the key and allows me to find items without any iteration.
To be clear, I'm thinking of something like this:
const users = {
129865: {
name: 'Zackary Solomon',
id: 129865,
},
198576: {
name: 'Salma Berg',
id: 198576,
},
// -- etc
}
const getUserById = (id) => {
return users[id]
}
Now, instead of sifting through the users
each time, I can access a user directly if I know their id:
const salma = getUserById('198576')
Nice. This works much better for frequent lookups. Additionally, the helper function makes it clear what we're doing and keeps any future refactoring in one place.
Transforming the array to an object
A good question would be, how do we convert the array to the lookup version above? Thankfully, it requires minimal effort and it can also be packaged up into a function:
const transformArrayToLookup = (lookupKey, array) => {
const transformed = {}
array.forEach((item) => {
const key = item[lookupKey]
transformed[key] = item
})
return transformed
}
const lookup = transformArrayToLookup('id', users)
// here we return the user by accessing it with the id, instead
// of performing a `find`
const getUserById = (id) => {
return lookup[id]
}
So, we loop through the array once and return a new object with the chosen key
(in this case id
) as the property we use to 'lookup' a user.
This could be made a little less verbose with a bit of help from reduce
, but
as I grow older and wiser I get more confident that reduce is just not worth the
overhead for people trying to understand it.
Okay, so are we done here? Technically yes, this works just fine. But, before you leave I thought we could try and make it even neater.
Using the brand new Object.groupBy
In case you didn't know, both Object.groupBy
and Map.groupBy
have recently
made it to TC39 Stage 4 proposal
and this indicates that:
"the addition is ready for inclusion in the formal ECMAScript standard"
It's actually ready for use now in everything except the very latest Safari:
What does it do?
To put it simply, it's designed to group objects by a common key. I'll use the example from the MDN page to demonstrate
const inventory = [
{name: 'asparagus', type: 'vegetables', quantity: 5},
{name: 'bananas', type: 'fruit', quantity: 0},
{name: 'goat', type: 'meat', quantity: 23},
{name: 'cherries', type: 'fruit', quantity: 5},
{name: 'fish', type: 'meat', quantity: 22},
]
const result = Object.groupBy(inventory, ({type}) => type)
/* Result is:
{
vegetables: [
{ name: 'asparagus', type: 'vegetables', quantity: 5 },
],
fruit: [
{ name: "bananas", type: "fruit", quantity: 0 },
{ name: "cherries", type: "fruit", quantity: 5 }
],
meat: [
{ name: "goat", type: "meat", quantity: 23 },
{ name: "fish", type: "meat", quantity: 22 }
]
}
*/
If you look closely you can see it almost does what we created with the
transformArrayToLookup
, but the crucial difference is the value of each key is
an array, not an object. Luckily we hid our implementation for retrieving a user
inside a function, because we're smart like that. It will be a quick change to
return the object from the array as we're only ever expecting it to have a
single item.
A bit of refactoring
Let's make some changes then, first by making use of groupBy
and then ensuring
that we get an object back, and not an array:
const transformArrayToLookup = (lookupKey, array) => {
return Object.groupBy(array, (item) => {
return item[lookupKey]
})
}
const lookup = transformArrayToLookup('id', users)
const getUserById = (id) => {
return lookup[id][0] // <--- this ensures we get the object as before
}
// works!
const salma = getUserById('198576')
This allows a terser approach to creating the lookup (compared to a forEach
or
reduce
) but it could be argued that it sacrifices some readability if other
engineers are not familiar with groupBy
. It also relies on getUserById
returning the first index which is not overly ergonomic.
I'll let you decide which approach you prefer.
Resources
- Working solution on TS playground - https://tsplay.dev/wQLvjW
- MDN page on
Object.groupBy