Why { ...defaultValues, ...newValues } can hide a bug
Tags: javascript, softwaredevelopment, webdev, programming
Published: 2024-03-22 14:39:10

Using the [spread operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_object_literals) 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.

```javascript
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.

```javascript
// { 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](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), you have a bug in the previous code.

Given the same last scenario, but a different object `newValues`.

```javascript
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? :eyes:
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.

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) to check if the second element is [truth](https://developer.mozilla.org/en-US/docs/Glossary/Truthy).

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/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.


```javascript
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! :wave: