Using the spread operator to create a new object using other objects is not unusual, but what happens if both objects have the same key?
TL;DR: This article explores a common issue with the spread operator in JavaScript when merging objects with overlapping keys and potentially falsy values. It explains how the spread operator can override default values with null or falsy values, leading to unexpected behavior. The article offers two solutions: a simpler one that directly checks and assigns values based on truthiness and a more scalable approach using
Object.entries
andObject.fromEntries
to filter out falsy values before merging.
The scenario
You are creating a new object in Javascript with default values and want to update it with new values from another object.
Knowing that JavaScript has spread operator since ES6, you use the following structure.
const defaultValues = {
foo: "Bar",
number: 30,
fruit: "banana"
}
const newValues = {
foo: "Baz",
number: 40,
}
const outputValues = {
...defaultValues,
...newValues
}
What is the expected output?
You are right if you say that the newValues
object will update the outputValues
object.
// { foo: "Baz", number: 40, fruit: "banana" }
Great, so it's done, right?!
Hmm... maybe not
The issue
When using the spread operator to create a new object, we are not making any validation about the values coming from the newValues
object, which can cause some weird results.
The possible problem
If you need to have the defaultValues
object to be replaced only by truthy Javascript values, you have a bug in the previous code.
Given the same last scenario, but a different object newValues
.
const defaultValues = {
foo: "Bar",
number: 30,
fruit: "banana"
}
const newValues = {
foo: "Baz",
number: 40,
fruit: null
}
const outputValues = {
...defaultValues,
...newValues
}
What is the expected output?
If you expect to have fruit: "banana"
think twice.
The final value will be fruit: null
.
Why? 👀
Based on the spread operator MDN definition
Overriding properties
When one object is spread into another object, or when multiple objects are spread into one object, and properties with identical names are encountered, the property takes the last value assigned while remaining in the position it was originally set.
This means that the spread operator does not validate if the value is or isn't a truth value.
And how do we solve it?
Well, there's the simple, non-scalable solution and the more complex, but scalable solution. Let's discuss each of them.
Simpler solution
Let's create a simple validation for the fruit
key.
Getting back to the last scenario, but with a change in the fruit value.
const defaultValues = {
foo: "Bar",
number: 30,
fruit: "banana"
}
const newValues = {
foo: "Baz",
number: 40,
fruit: null
}
const outputValues = {
...defaultValues,
...newValues,
fruit: newValues.fruit || defaultValues.fruit
}
Ok, it can work in a few validations but is not scalable at all, so let's create a validation for each value in the newValues
object.
Scalable solution
To make it scalable for whatever the object size, let's use the methods entries from Object
, to parse the values.
Using this method, we will have an array of [key, value]
, that we can use the arrays method filter to check if the second element is truth.
Object.entries(newValues).filter(item => Boolean(item[1]))
// [ ['foo', 'Baz'], ['number', 40] ]
Perfect, now we have validations for falsy values in the object, but I need an object, not an array, genius.
Calm down, there's another Object
method so solve it, the fromEntries method.
From MDN: "The Object.fromEntries() static method transforms a list of key-value pairs into an object.", and that's the magic, take a look.
const defaultValues = {
foo: "Bar",
number: 30,
fruit: "banana"
}
const newValues = {
foo: "Baz",
number: 40,
fruit: null
}
const outputValues = {
...defaultValues,
...Object.fromEntries(
Object.entries(newValues)
.filter(item => Boolean(item[1]))
),
}
What is the expected output?
Yes, you're right, now the output is the same as the simpler solution, but you can use in bigger objects.
The final value is: {foo: 'Baz', number: 40, fruit: 'banana'}
.
Conclusion
In conclusion, while the spread operator in JavaScript offers a convenient way to merge objects, it can lead to unexpected behavior when dealing with overlapping keys and potentially falsy values. The issue arises because the spread operator does not discriminate based on the truthiness of merged values. This can result in undesired outcomes, such as overriding default values with null or other falsy values.
Check out my links
Website: https://www.guimoraes.dev/
Linkedin: https://www.linkedin.com/in/guimoraesdev/
If you find any error or just want to say hi, let a comment below.
See ya! 👋
Top comments (4)
Hmm.. interesting. Typically, I only want to override the defaults if the new value is
undefined
(not included in the object). If I only rely on truthy values, then I can't override a default value that istrue
withfalse
for example.Anyway, this is definitely helpful to understand so you're not surprised by the behavior.
nullish coalesce would seem appropriate here. Like this...
fruit: newValues.fruit ?? defaultValues.fruit
Yes, it's a great addition, and can prevent issues with falsy values, like numbers of false statement
Informative article, thanks for writing!