DEV Community

Guilherme Moraes
Guilherme Moraes

Posted on

Why { ...defaultValues, ...newValues } can hide a bug

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 and Object.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
}
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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 defaultValuesobject 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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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] ]
Enter fullscreen mode Exit fullscreen mode

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]))
        ),
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
kiliman profile image
Kiliman

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 is true with false for example.

Anyway, this is definitely helpful to understand so you're not surprised by the behavior.

Collapse
 
geoffswift profile image
Geoff Swift

nullish coalesce would seem appropriate here. Like this...

fruit: newValues.fruit ?? defaultValues.fruit

Collapse
 
guimoraes profile image
Guilherme Moraes

Yes, it's a great addition, and can prevent issues with falsy values, like numbers of false statement

Collapse
 
best_codes profile image
Best Codes

Informative article, thanks for writing!