What am I ranting about? I’m talking about this:
const obj = {
...condition && { prop: value },
};
Trust me, this is perfectly acceptable and executable JavaScript.
Surprised? Shaken up? Aghast? Maybe just intrigued?
Bear with me for few lines, I’ll try to explain what’s going on thanks to the ECMAScript Language Specification.
It won’t be so boring, I new Promise()
.
Example Link to heading
I’ll start with an example to clarify the situation.
Here a weird and not to be imitated at home query
object is being constructed taking values from the result of a form previously submitted.
Only two fields are mandatory: the requested collection and the sort ordering, which for simplicity are hardcoded.
On the contrary, the state and the priority could be absent in the formValues
object, so they should be conditionally inserted into the query
object.
const state = formValues['state'];
const priority = formValues['priority'];
const query = {
collection: 'Cats',
sort: 'asc',
...state && { state },
...priority && { priority },
};
await doQuery(query);
If the formValues
object doesn’t own one or more of the conditional properties, not even the resulting query
object will have it/them.
Explanation Link to heading
An insight into the spec Link to heading
When such a case is encountered by a JavaScript engine, the spec leaves no room for doubt. Here we can see that the CopyDataProperties
abstract operation has to be performed.
Performed on what? Why parenthesis are not needed? What is an abstract operation?
One thing at a time, dear reader.
Following the same link, four lines above, we can see that whatever follows the spread operator, it must be an AssignmentExpression
. No need for parenthesis. What is an AssignmentExpression
? It could be many things, also an arrow function! However, our case is based on a simple ConditionalExpression
.
The spec says the expression should be evaluated and the result must be fed to the CopyDataProperties
abstract operation. Therefore, properties will be copied from the result of the evaluation to the object literal on which we are working.
Now we can define what is an abstract operation: a list of tasks performed internally by the JavaScript engine. Later we will focus more on those that compose the CopyDataProperties
abstract operation.
Let’s recap what we learned so far:
- the conditional expression will be immediately evaluated
- the result of that evaluation will be taken by the
CopyDataProperties
abstract operation, which is responsible of the properties cloning and insertion
The logical && operator Link to heading
Let’s focus on the conditional expression.
The value produced by the && operator will always be the value of one of the two operand expressions. It is not necessarily of type Boolean. If the first operand results in a truthy value, the && expression results in the value of the second operand. If the first operand results in a falsy value, the && expression results in the value of the first operand.
let expr1 = 'foo';
let expr2 = null;
let expr3 = 42;
// the first operand is a truthy value -> the second operand is the result
expr1 && expr2; // null
expr1 && expr3; // 42
// the first operand is a falsy value -> the first operand is the result
expr2 && expr1; // null
expr2 && expr3; // null
Therefore, what if our condition is a truthy value? We could transform the initial code:
const obj = {
...condition && { prop: value },
};
into:
const obj = {
...{ prop: value },
};
We don’t need to know what will the CopyDataProperties
abstract operation do to understand the final result: the inner object will be spreaded and its property will be cloned into obj
.
On the contrary, what if our condition is a falsy* value? We run in the following situation:
const obj = {
...condition,
};
And here’s where things get interesting.
The CopyDataProperties abstract operation Link to heading
Here we can see what are the steps followed by the abstract operation.
The point number 3 says something newsworthy: if a null value or an undefined value will be encountered, no operation will be performed. So we can end up in the situation where the condition results into null or undefined with no problems:
const obj = {
...null,
};
and:
const obj = {
...undefined,
};
are equivalent to:
const obj = {
};
If we jump to the points number 5 and 6 we can see that each own property will be cloned if our condition would result into an object. We know that all the objects are truthy values, also empty ones, so at the moment we can ignore this case. In fact, do you remember what happen if the condition would be a truthy value?
Finally, what if the condition results into one of the remaining falsy primitive values?
Focus on the point number 4. Do you see the intervention of the toObject
abstract operation? Let’s take a look!
We can ignore the first two cases because we already know that the CopyDataProperties
abstract operation ends before in such situations.
The last case assures us that if the argument is already an object, no harm will be done to it. But even this cannot happen.
Instead, what happens if the argument is one of Boolean, String, and Number? Simple: it will autoboxed into the corrispondent wrapper object.
It is worth noting that, in our case, the resulting wrapper objects have no own properties. Boolean and Number wrapper objects store their value into an internal, and inaccessible, property. On the contrary String wrappers do expose the contained characters (read-only), but remember that only an empty string is a falsy value.
No own properties means the end of the CopyDataProperties
abstract operation, which will have no properties to clone.
So we can transform the last partial result:
const obj = {
...condition,
};
into:
const obj = {
...{},
};
Without any side effect!
Conclusion Link to heading
I hope I was able to explain everything in the best possible way ๐
English is not my mother tongue, so errors are just around the corner. Feel free to comment with corrections! I hope to see you there again ๐ and on twitter!
ย
* One of false, 0, empty string, null, undefined and NaN.