oli's profile picture
Article3.2 minute read

Two ways to conditionally set object properties

A junior developer recently asked me how they could conditionally set a property on an object. They didn't want an always-present key with a value of `undefined`, they wanted the property not to exist at all.

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:

  1. Expressions
  2. Short circuit evaluation
  3. Object spread

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.