There's a super fancy concise modern way to do this, and a simple "verbose" way.
Conditional spread expressions
A "conditional spread expression" (I just made that term up) looks like this:
const obj = {
test: "one",
...(condition && { other: "two" }),
};
You may be thinking "wow that is confusing and unreadable". You would be right, but let's unpack exactly what's going on for the sake of the blog post.
There are a few different language features combining here:
Expressions
The bit after the ...
is an expression. Expressions are bits of code that produce a value, e.g. 1 + 3
resolves to 4
. The expression here will be evaluated to a value first.
Short circuit evaluation
The "logical AND" (&&
) operator checks if the left side is truthy and evaluates to the right hand side if so. E.g. 1 === 1 && "hello"
evaluates to "hello"
.
If the left side is falsy then it "short circuits" and skips evaluating the right side. The expressions resolves to the left side instead. E.g. 1 === 2 && "hello"
evaluates to false
and never runs the "hello"
code.
Our conditional spread relies on this behaviour. If our condition is truthy the expression evaluates to { other: "two" }
. If our condition is falsy the expression evaluates to the condition, which could be any one of the 6 falsy primitives (false
, undefined
, null
, 0
, NaN
or ""
).
Object spread operator
Finally we need to understand some nuance around the object spread operator. It allows us to take all the properties from one object and "spread" them into another object.
When our condition is truthy this means { other: "two" }
is spread into obj
, resulting in:
const obj = {
test: "one",
other: "two",
};
It's a bit more confusing to understand what happens when our condition is falsy. The spread operator spec says that spreading undefined
or null
should do nothing. So we end up with:
const obj = {
test: "one",
};
Spreading a boolean, string or number (including NaN) will first convert the value into its corresponding wrapper object (Boolean
, String
or Number
). These objects will have no properties, so we are effectively spreading {}
:
const obj = {
test: "one",
...{},
};
Spreading an empty object will add no new properties to the parent object.
Sidenote for completeness
Technically spreading a non-empty string will convert the string into an object with all its characters as properties (e.g. {..."cat"}
results in { 0: "c", 1: "a", 2: "t" }
). Since only empty strings are falsy we can ignore this behaviour here.
A simpler way
We can avoid having to understand most of the arcane spread behaviour by using a slightly longer syntax:
const obj = {
test: "one",
...(condition ? { other: "two" } : {}),
};
This way we are always spreading an object, and it should be relatively intuitive that spreading an empty object won't do anything.
Keep it simple
Alternatively we could not require our co-workers to read the TC39 spec documents:
let obj = {
test: "one",
};
if (condition) {
obj.other = "two";
}
This has the advantage of allowing multiple properties to be set based on the same condition.
let obj = {
test: "one",
};
if (condition) {
obj.other = "two";
obj.otherRelatedthing = "three";
}
I also quite like the clearly marked conditional block. When reading the code I can look at the condition, see it's not relevant to what I'm working on and ignore the entire block.
Of course if you have lots of properties that need to be set based on different conditions this will get much more verbose, so the conditional spread version might be a better choice then. As always, context is important.