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
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
An insight into the spec
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
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
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
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!
* One of false, 0, empty string, null, undefined and NaN.
Discussion (49)
Why should I choose the shortest way if it needs an article to be explained? In my opinion, the best code is the self-explanatory one, not the shortest one
I completely disagree because lot of JS syntax and features need an explaination. Think about coercion, generators, arrow functions, async await, the asynchronous iteration, proxies, ecc.
More generally each concept in every programming language, also the basic ones, may need an explanation.
And, with your line of thinking, people risk to never learn anything and never improve themselves.
The article is explaining why the syntax is valid. But the main concept, the optional insertion of a property, is understandable in 10 seconds.
After a little example, if you spend 5 mins with it, you can already understand what's going on:
...condition && {prop:value}
If the condition is true a prop will be added. No side effects in the other case. Simple.
It's something with which one can become familiar in no time, it's something immediately teachable to others and I've demostrated that it works not because an undefined, untrustable behaviour but thanks to a precise logic put black on white in the spec.
I don't criticize the use of idiomatic code (I often use it, too) but the emphasis on the shortest way.
It seems to encourage the writing of compact or idiomatic code at any cost, and it is not always the best choice.
As with natural language, if you use very strict jargon, you reduce the number of people who are able to understand it with little effort.
The shortest way means only one thing: fewer characters are required.
The second part you deduced is your personal interpretation of the title, which obviously does not reflect my thinking at all.
Maybe you're right. Sorry for any misunderstanding.
No problems 😃!
See you at the DevFest, with peaceful intentions of course 😝
I've never had any non-peaceful intentions :-)
See you at the DevFest!
This is referred to as the "either/or fallacy", also referred to as the "false dilemma": en.wikipedia.org/wiki/False_dilemma
This is a perfectly self-explanatory technique. If you understand the spread syntax, and how short-circuted Boolean expressions work, then you can see at a glance what this does. The text above is not an explanation of the code, but proof that it works without surprises. Hence, this is both the shortest and quite self-explanatory.
I would even go so far as to say that this is a lot more readable than the alternatives. Bravo, Mr. Costa.
At no point does the author claim that short code should supersede readable code.
You centered the point, thank you for intervening!
P.S. I think it's very readable too, but I'm realizing, reading a great number of opinions and comments even on a small thing like the one I presented in the article, how readability is a subjective thing.
I like this pattern not just because it's the "shortest" but because it's the most declarative. This way, I can construct and assign my object in one place, without going back later to mutate it's contents. I think that leads to better code because it reduces the need to mentally execute each line.
It's called an "idiom". Like any other idiom, including idioms in naturally language, you have to learn it. That doesn't mean it's not useful. If people run into it and are not familiar with it, they'll figure it out quickly and then know it themselves.
I completely agree with you.
Interesting trick, but what if our value is 0 which can be a valid value to use? So our code:
won't copy anything.
Just make sure to internalize what JavaScript considers truthy and you won't think this is a "gotcha" anymore.
So in your specific case you'd probably want to do something like
AFAIK, this is considered as a bad practice to use
typeof
to check the number, asNaN
is also has typenumber
(e.g.typeof NaN === "number"; // -> true
). Moreover,typeof
when used against number will returnnumber
, as a result, in lower case (i.e. notNumber
butnumber
).Yeah the casing of
number
was just a typo. Adjusted it to check for NaN.So there is no point to use the trick in this way if 0 is a valid amount as well.
Remember that on the left you put the condition, so you may write like the following:
you could pass coins to a method that checks for false, null, and undefined only (or something along those lines)
It's a useful trick! There are a bunch of others at blog.bitsrc.io/6-tricks-with-resti... (not my post), including a concise way to remove properties from objects.
I believe in a quote, attributed to Brian Kernigan that goes like:
"Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?"
In languages like javascript, there is a minifier anyway (and a transpiler most probably) in front of our code and the production code that gets served in the browser (unless of course your write code for the server-side).
To be fair, the above is not the worst example of "smart" code that I have seen and the article is a nice explanation of why this works so thanks for that!
I've already expressed mine opinion here around, and to be technically precise the code is easily testable as any if construct.
Thanks for sharing your opinion!
I do find the article helpful but to be honest with you, it was the first time I saw something like this.
I guess someone that is more experienced with the spread operator and augmenting objects in this way might have seen it more than me.
As you said, it is most probably a matter of opinion as a lot of things in javascript are those days, for example, I find something like this pretty hard to read:
let adder = (x) => (y, z) => x + y + z;
compared to the old-style alternative but there are people who love it.
So thank you again for going deep in your explanation :)
You're welcome!
To me, more terse code is almost always easier to parse. Of course, everyone's different. There's not really a one size fits all solution. You'll have to agree with your team on a working standard.
This seems much nicer than my current
const obj = { ...{ condition? { prop: value }: {} }};
🤔While it's not entirely clear at first glance how it works, I think the intent is clear enough for someone stumbling across it (excusing any bugs caused by gotchas)
That's awesome. Thanks for posting.
I didn't know that the rest operator could take an expression (though it does make sense).
I think your example could be better though. As said you'd quickly run into unexpected behaviour with falsey primitive values, which you mention but downplay. It's a showstopper imo.
As a shortened, silly example showing what could happily ruin your day in longer, serious code...
Most coders would assume a method would treat an empty string like any other.
I'd just code the example object as...
Just simpler and less brittle.
A better example might use something where the inserted properties are computed. a la...
I think you will understand that this is beyond the scope of the article.
My first point was to show a nice, little know js fact. The second was to explain why such code is allowed.
All the rest is left to the reader's good will and curiosity :D
In most cases, just defaulting the property value to
undefined
is the easier choice.In the case of Firebase, to take one example, a value of undefined is treated differently than a missing property, and causes an error.
This makes total sense. I usually just build the object then run a sanitize function on it. No wonder why I never stoped to think about this.
Thanks for posting, simple trick but mind opening (mainly if you are biased by different approaches like me).
I do love find alternative ways to do common things 😆, I'm honored to have opened your mind 😝
Basically, it evaluates like this:
...(condition && { prop: value }),
Very helpful what Stan posted here. Parenthesis really help. Actually they are a default lint recommendation in this specific case. I think adding them to the code in the article would help a lot.
But it wouldn't be the shortest anymore ;)
hey man! I'm trying to help you here! lol...great article!
Personally, I would do it like
...(obj && { props: foo})
so it is obvious how the operators work, people who don't immediately know that&&
has precedence over...
won't read the code as easily.This is a great explanation. Thanks for digging in and explaining what was happening.
Thank you!
For added fun with this, I tried a couple of things:
Mind blown!
Interesting article, I had always found that behavior a bit eery and never bothered looking into the why. Thanks for sharing 🙏
You're welcome!
I know how && works, but wtf does ... do?
Google rest/spread operators 😆
It unpacks stuff, like * in Python, which "spread" could be a synonym for, but "rest operator"? googles Ah, as in "remaining parameters".
I'm a year to late at the party, but thank you very much for the good explanation. I was in search for exactly this feature and it is an elegant and modern solution!
Thanks!
Typo: semplicity
~semplicity~ simplicity