Introduction
At work recently, I encountered an interesting problem: I needed to merge multiple JavaScript objects together in an array based on their matching keys.
It sounds relatively straightforward at first, but there's more to the story that prevented me using the spread operator or Object.assign()
to combine these objects.
- Each of these objects might share a common key - it wasn't guaranteed, and
- Although they all had the same properties in each object, not all of these properties were defined in every object - the object could have
property: value
or it could beproperty: undefined
.
I needed to combine all the properties of objects that shared a common key along with their defined values into a new single object. And to make it just a smidge more difficult I needed it to work in TypeScript. ๐
So today, I'll show you how to use JavaScript's reduce()
function to merge two (or more) objects in an array with different defined properties.
Background: motion.qo
and air.qo
events
For those of you curious as to when this sort of thing comes up in real life, here's a little more context about my situation.
I work as a software engineer at an Internet of Things (IoT) startup named Blues Wireless. Our main mission is to make IoT development simpler, regardless of if there's a reliable Internet connection or not. Blues does this via Notecards - prepaid cellular devices that can be embedded into any IoT device "on the edge" to transmit that sensor data as JSON to a secure cloud: Notehub.
In addition to producing this hardware and providing the secure cloud connection to send IoT data to, we also build software applications our users can connect their devices to and see the sensor data in charts and graphs.
Here's an example of the charts displaying various sensor data.
One of the web apps we were building involved sensors that reported two types of data:
- air quality readings like temperature, humidity, sensor voltage, and pressure, and
- motion readings like current count of motions detected and total motions detected over time.
So a single sensor is generating two different JSON payloads that are sent from a Notecard to Notehub, then Notehub routes them to our web app to get rendered in the charts.
Here's a sample of some of the events delivered to the app.
Example sensor events
const sensorEvents = [
{
sensorId: 'abc',
id: '1',
humidity: undefined,
pressure: undefined,
temperature: undefined,
voltage: undefined,
count: 3,
total: 32,
lastActivity: '2022-02-10T00:41:11Z'
},
{
sensorId: 'abc',
id: '2',
humidity: 17.609375,
pressure: 96.768734,
temperature: 26.703125,
voltage: 3.24,
count: undefined,
total: undefined,
lastActivity: '2022-02-15T21:59:18Z'
},
{
sensorId: 'def',
id: '3',
humidity: undefined,
pressure: undefined,
temperature: undefined,
voltage: undefined,
count: 866,
total: 1776,
lastActivity: '2022-02-15T22:00:03Z'
}
]
As you can see, the first two objects in the array share the same sensorId
of abc
, but they have different defined properties.
The first object has motion
properties (and undefined air quality
properties), the second object has defined air quality
properties (and undefined motion
properties), and the third object has a different sensorId
entirely; that is a motion
reading from another sensor.
Which brought me to my problem: how to combine the objects with the same sensorId
and get all the defined properties from each object.
So, let's get to it.
Merge like objects with a mergeObject()
function and reduce()
With the amount of times I've said "merge" and "combine" in this post, you may already have figured out that the JavaScript array function reduce()
might come into play here, and it does.
At its most basic, reduce()
goes through each element in an array and adds it to an accumulator of all the values preceding it, until just one value is returned at the end. The same thing can be done with an array of objects, with just a little more logic.
If you'd like a more thorough explanation of
reduce()
, I encourage you to check out the Mozilla documentation.Their examples are great, and I still visit it regularly for all sorts of topics.
Below is the TypeScript-flavored solution I came up with to take an array of objects, find the ones that share a common key, and return a new array with those objects combined.
interface HasSensorId {
sensorId: string;
}
// merge objects with different defined properties into a single obj
const mergeObject = <V>(A: any, B: any): V => {
let res: any = {};
Object.keys({ ...A, ...B }).map((key) => {
res[key] = B[key] || A[key];
});
return res as V;
};
// use Map functions `get`, `set`, and `values`
// take each event, and make it into a new Map() obj where sensorId is the key
// regardless of if there's already a value for that key, run the merge function
// set the key and the new merge var as the key, value for the map
const reducer = <V extends HasSensorId>(
groups: Map<string, V>,
event: V
) => {
const key = event.sensorId;
const previous = groups.get(key);
const merged: V = mergeObject(previous || {}, event);
groups.set(key, merged);
return groups;
};
// run the sensor events through the reducer and then pull only their values into a new Map iterator obj
const reducedEventsIterator = sensorEvents
.reduce(reducer, new Map())
.values();
// transform the Map iterator obj back into an array
const reducedEvents = Array.from(reducedEventsIterator);
Ok, I realize there's quite a few functions above, so an explanation is in order.
HasSensorId
The HasSensorId
interface is pretty direct: it means that wherever this interface is referenced, the object's must have a string of sensorId
among the properties.
Now let's walk through the rest of these functions one by one.
mergeObject()
const mergeObject = <V>(A: any, B: any): V => {
let res: any = {};
Object.keys({ ...A, ...B }).map((key) => {
res[key] = B[key] || A[key];
});
return res as V;
};
This mergeObject()
function is the one that actually does the combining of objects.
The A
and B
objects that this function takes in are the two objects with a common sensorId
property, and the res
variable will be the newly returned object of their properties (and values) combined.
The first thing that happens is the two objects are combined using the spread operator, and then just their keys are extracted into its own array using Object.keys()
. If you passed the sample sensorEvents
array into this function, the Object.keys()
array would look like this:
[
'sensorId',
'humidity',
'pressure',
'temperature',
'voltage',
'count',
'total',
'lastActivity'
]
After the keys are extracted, each key is added to the new res
object as a key, and if the B
object's value at that key exists (B[key]
), it's added as the value for the res
object, otherwise, the A
object's value is added instead (even if it's undefined
).
The final res
object returned looks like this:
{
sensorId: 'abc',
humidity: 17.609375,
pressure: 96.768734,
temperature: 26.703125,
voltage: 3.24,
count: 3,
total: 32,
lastActivity: '2022-02-15T21:59:18Z'
}
And finally the V
and any
references are part of the TypeScript implementation. From looking at the rest of the code, we can see V
is an extension of the HasSensorId
interface, which means any event object will have sensorId
as a property. And any
means the objects passed in to the function could have any properties (or not), so it's flexible.
Ok, on to the next function!
reducer()
const reducer = <V extends HasSensorId>(
groups: Map<string, V>,
event: V
) => {
const key = event.sensorId;
const previous = groups.get(key);
const merged: V = mergeObject(previous || {}, event);
groups.set(key, merged);
return groups;
};
The reducer()
function makes excellent use of true Map
objects in JavaScript. It uses the HasSensorId
interface as well, ensuring that every object will have a sensorId
property.
Each event
object passed into the function, has its sensorId
turned into a key
variable. The previous
variable relies on Map's get()
function to fetch any other Map objects that have that same sensorId
key, and if another object is found, the new event
is combined with it when the mergeObject()
function is called. If there's no matching previous
object, an empty object is passed to mergeObject()
, and a new merged
variable is still returned.
Finally, the Map set()
function adds the key
and merged
variable to the groups
Map object.
If our three sensorEvents
are run through this function, the final groups
object that emerges looks like this:
Map(2) {
'abc' => {
sensorId: 'abc',
humidity: 17.609375,
pressure: 96.768734,
temperature: 26.703125,
voltage: 3.24,
count: 3,
total: 32,
lastActivity: '2022-02-15T21:59:18Z'
},
'def' => {
sensorId: 'def',
humidity: undefined,
pressure: undefined,
temperature: undefined,
voltage: undefined,
count: 866,
total: 1776,
lastActivity: '2022-02-15T22:00:03Z'
}
}
And so we move on to the penultimate part of this code example, reducedEventsIterator
.
reducedEventsIterator
const reducedEventsIterator = sensorEvents
.reduce(reducer, new Map())
.values();
The reducedEventsIterator
variable is where we call reduce()
on the sensorEvents
array. The reducer()
function gets passed as the first argument each event in the array must run through, and a new Map()
object is passed as the accumulator object where they will all be gathered together at the end.
Before the Object.values()
method is chained on to the end, the array looks like this:
[
[
'abc',
{
sensorId: 'abc',
humidity: 17.609375,
pressure: 96.768734,
temperature: 26.703125,
voltage: 3.24,
count: 3,
total: 32,
lastActivity: '2022-02-15T21:59:18Z'
}
],
[
'def',
{
sensorId: 'def',
humidity: undefined,
pressure: undefined,
temperature: undefined,
voltage: undefined,
count: 866,
total: 1776,
lastActivity: '2022-02-15T22:00:03Z'
}
]
]
After .values()
is called, the final reducedEventsIterator
looks more like this:
[Map Iterator] {
{
sensorId: 'abc',
humidity: 17.609375,
pressure: 96.768734,
temperature: 26.703125,
voltage: 3.24,
count: 3,
total: 32,
lastActivity: '2022-02-15T21:59:18Z'
},
{
sensorId: 'def',
humidity: undefined,
pressure: undefined,
temperature: undefined,
voltage: undefined,
count: 866,
total: 1776,
lastActivity: '2022-02-15T22:00:03Z'
}
}
Which brings us to the final step: transforming this Map of objects back to a proper JavaScript array of objects (and all the benefits that entails - arrays are much easier to manipulate).
The final merged array: reducedEvents
const reducedEvents = Array.from(reducedEventsIterator);
Arguably the simplest part of this whole tutorial, the Map of objects is transformed back into an array simply by passing it into the Array.from()
method.
Beware
Array.from()
for copying deep objectsIf you have deeply nested objects in some sort of array-like structure that you need to copy,
Array.from()
probably won't work for you. It only makes shallow-copied instances of arrays.
So if we pass the example sensor events displayed above into our reducedEventsIterator()
function here, this is what the final result looks like:
[
{
sensorId: 'abc',
humidity: 17.609375,
pressure: 96.768734,
temperature: 26.703125,
voltage: 3.24,
count: 3,
total: 32,
lastActivity: '2022-02-15T21:59:18Z'
},
{
sensorId: 'def',
humidity: undefined,
pressure: undefined,
temperature: undefined,
voltage: undefined,
count: 866,
total: 1776,
lastActivity: '2022-02-15T22:00:03Z'
}
]
And there you have it: an array of freshly merged objects with all available properties present.
Conclusion
One reason I enjoy being a software engineer is because of all the interesting problems I get to solve regularly. Lately, when I was building a web app to display data from IoT sensors at work, I ran into a more unique problem: taking an array of sensor events and merging the events together if they belonged to the same sensor. The catch was, all the events had the same properties, but depending on which event you were looking at, half the properties had values of undefined
.
So instead of being able to use the spread operator or Object.assign()
to get these events merged into one object, I had to get more creative with a few extra functions and the reduce()
method. But it worked - even with TypeScript in the mix.
Check back in a few weeks โ Iโll be writing more about JavaScript, React, IoT, or something else related to web development.
If youโd like to make sure you never miss an article I write, sign up for my newsletter here: https://paigeniedringhaus.substack.com
Thanks for reading. I hope this little bit of JavaScript helps you out in future - who'd have thought merging objects could be so interesting, right?
References & Further Resources
- Spread operator Mozilla docs
- Object.assign() Mozilla docs
- Map object Mozilla docs
- Blues Wireless website
- JavaScript array method reduce()
Top comments (0)