javascript: Nested ternaries aren't bad, just misused

The argument against nested ternaries is that they can make code more difficult to understand. But they can make code easier to understand if they are wielded properly. Instead of banning nested ternaries, require that they are formatted such that each line has a single condition and a single result, with the catch-all at the end:

// bad
const foo = maybe1 > maybe2
  ? "bar"
  : value1 > value2 ? "baz" : null;

// good
const foo =
  maybe1 > maybe2 ? "bar"
  : value1 > value2 ? "baz"
  : null;

This is similar to a switch statement that always breaks. With this pattern, it’s impossible to get lost in nested contexts. Once you’ve ruled out a line applying to the case you’re considering, you can safely forget it. It’s easier to understand than the solutions provided by the style guide right now, and also saves a memory allocation.

Here are some other examples where this pattern improves code:

// bad
let result;
if (operator === ‘+’) {
  result = left + right;
} else if (operator === ‘*’) {
  result = left * right;
} else if (operator === ‘-’) {
  result = left — right;
} else {
  result = left / right;
}

// good
const result =
  operator === ‘+’ ? left + right
  : operator === ‘*’ ? left * right
  : operator === ‘-’ ? left — right
  : left / right;
  • Less code.
  • Prefers const over let.
  • Avoids side effects (i.e. assigning value of variable outside block scope).
// bad
if (conditionA) {
  outcome1();
} else if (conditionB) {
  if (conditionC) {
    outcome2();
  } else {
    outcome3();
  }
} else {
  outcome4();
}

// good
conditionA ? outcome1()
: conditionB && conditionC ? outcome2()
: conditionB ? outcome3()
: outcome4();
  • Less code.
  • Less cognitive load.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 11
  • Comments: 30

Most upvoted comments

First of all, things that are easily misused / are easy to improperly wield are bad.

A switch statement isn’t a good idea to use ever either, because you can easily forget to always break. In the future, switch statements will be discouraged by this guide as well. (Separately, memory allocation is irrelevant; JS is a memory-managed language, which means it’s not the developer’s business to think about it).

The pattern you’re suggesting still has the same issues with intuition about precedence, so it doesn’t address the problem. In particular, I don’t agree that any of your “good” examples are easier to read; you’re also abusing && for control flow. The if/else example is far better.

@mk-pmb Sure, you could write:

const getResult = (operator) => {
  if (operator === ‘+’) {
    return left + right;
  }
  if (operator === ‘*’) {
    return left * right;
  }
  if (operator === ‘-’) {
    return left — right;
  }
  return left / right;
};

const result = getResult(operator);

But this code is still much better:

const result =
  operator === ‘+’ ? left + right
  : operator === ‘*’ ? left * right
  : operator === ‘-’ ? left — right
  : left / right;

It’s easier to read and understand, takes less time to read and maintain, and has less surface area in which bugs can hide. The fact that it’s also much more performant is just the cherry on top.

Separately, memory allocation is irrelevant; JS is a memory-managed language, which means it’s not the developer’s business to think about it

Just for the record, I work at a JavaScript game studio, and it absolutely is the business of all of our engineers to think about memory allocation.

I read that example to basically be the same as conditionA && outcome1() || (conditionB && conditionC) && outcome2() .... You’re using what is normally used as an assignment, instead as flow control.

But this code is still much better: […] It’s easier to read and understand, takes less time to read and maintain,

Looks like a matter of what you’re used to, then. For me, those chained ternaries are harder to read. They also evoke self-doubt about whether I remember all of the operator precedence rules correctly, which slows down my work or even prevents me working on that part when my brain isn’t at full energy.

I consider some table-style layout way more intuitive:

const result = (() => {
  if (operator === '+') { return left + right; }
  if (operator === '*') { return left * right; }
  if (operator === '-') { return left - right; }
  return left / right;
})();

Would be even clearer with a real table lookup:

const res2 = ({
  '+': (a, b) => (a + b),
  '*': (a, b) => (a * b),
  '-': (a, b) => (a - b),
  '/': (a, b) => (a / b),
})[operator](left, right);

Free debug bonus: % and ^ throw an exception instead of just using division.

Maybe there could even be some performance gains in factoring out that anonymous table into one object shared between all invocations of the function that uses it.