48

I personally love ternary operators, and in my humble opinion, they make complicated expressions very easy to digest. Take this one:

  const word = (distance === 0) ? 'a'
    : (distance === 1 && diff > 3) ? 'b'
    : (distance === 2 && diff > 5 && key.length > 5) ? 'c'
    : 'd';

However in our project's ESLINT rules nested ternary operators are forbidden, so I have to get rid of the above.

I'm trying to find out alternatives to this approach. I really don't want to turn it into a huge if / else statement, but don't know if there's any other options.

dthree
  • 18,488
  • 11
  • 69
  • 103
  • 1
    Perhaps a `switch` statement as it looks like there a a few scenarios that you want to evaluate: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch – R.A. Lucas Aug 29 '15 at 18:26
  • 3
    If you think they make complicated expressions easy to digest, just disable that stupid eslint rule. – Bergi Aug 29 '15 at 18:26
  • 1
    I'm doing it based on an eslint ruleset that isn't mine to decide. – dthree Aug 29 '15 at 18:27
  • Maybe check [this approach](http://stackoverflow.com/a/31873114/1048572) if you want to avoid `if/else` and also like expressions – Bergi Aug 29 '15 at 18:34
  • @Bergi: Yeah, I couldn't find a reasonable lookup map option relative to a simple `if`/`else`. – T.J. Crowder Aug 29 '15 at 18:36
  • 1
    @T.J.Crowder: It could be `['a', res.difference > 3 && 'b', res.difference > 5 && String(res.key).length > 5 && 'c'][res.distance] || 'd'`, but whether we'd call that "reasonable" is questionable :-) – Bergi Aug 29 '15 at 21:08

15 Answers15

38

Your alternatives here are basically:

  1. That if/else you don't want to do
  2. A switch combined with if/else

I tried to come up with a reasonable lookup map option, but it got unreasonable fairly quickly.

I'd go for #1, it's not that big:

if (res.distance == 0) {
    word = 'a';
} else if (res.distance == 1 && res.difference > 3) {
    word = 'b';
} else if (res.distance == 2 && res.difference > 5 && String(res.key).length > 5) {
    word = 'c';
} else {
    word = 'd';
}

If all the braces and vertical size bother you, without them it's almost as concise as the conditional operator version:

if (res.distance == 0) word = 'a';
else if (res.distance == 1 && res.difference > 3) word = 'b';
else if (res.distance == 2 && res.difference > 5 && String(res.key).length > 5) word = 'c';
else word = 'd';

(I'm not advocating that, I never advocate leaving off braces or putting the statement following an if on the same line, but others have different style perspectives.)

#2 is, to my mind, more clunky but that's probably more a style comment than anything else:

word = 'd';
switch (res.distance) {
    case 0:
        word = 'a';
        break;
    case 1:
        if (res.difference > 3) {
            word = 'b';
        }
        break;
    case 2:
        if (res.difference > 5 && String(res.key).length > 5) {
            word = 'c';
        }
        break;
}

And finally, and I am not advocating this, you can take advantage of the fact that JavaScript's switch is unusual in the B-syntax language family: The case statements can be expressions, and are matched against the switch value in source code order:

switch (true) {
    case res.distance == 0:
        word = 'a';
        break;
    case res.distance == 1 && res.difference > 3:
        word = 'b';
        break;
    case res.distance == 2 && res.difference > 5 && String(res.key).length > 5:
        word = 'c';
        break;
    default:
        word = 'd';
        break;
}

How ugly is that? :-)

T.J. Crowder
  • 959,406
  • 173
  • 1,780
  • 1,769
  • @RayonDabre: There are multiple different ways we arrive at `'d'`, which makes the up-front assignment the simplest. Otherwise, we have a conditional in both the `1` and `2` cases and repeat `'d'` three times; the former doesn't bother me that much, but the latter is a maintenance problem waiting to happen. – T.J. Crowder Aug 29 '15 at 18:33
  • Perfect ! **I guess thats the difference between the approaches of average developer and T.J. Crowder !** – Rayon Aug 29 '15 at 18:37
  • @T.J.Crowder thanks for the great answer. I think you've successfully sold me to further embrace `if / else` statements. Sure beats the switch statements in my opinion. :) – dthree Aug 29 '15 at 18:38
  • @dthree: Mine too. :-) If it weren't that we're testing three different things, we could have used a map, but... – T.J. Crowder Aug 29 '15 at 18:39
  • Haha! I'll keep map in mind for similar cases where it could apply. – dthree Aug 29 '15 at 18:40
  • Is *B-syntax language family* meant to be ***C*** *syntax...*, or is that some kind of definition / classification (that I'm unfamiliar with, and so is google ;) )? – Amit Aug 29 '15 at 20:23
  • 1
    @Amit: I think he really meant [B](https://en.wikipedia.org/wiki/B_(programming_language)). – Bergi Aug 29 '15 at 21:04
  • @Amit: Bergi is (characteristically) correct: The language tree in question goes back at least to BCPL, but it was B (a successor to BCPL) where the braces syntax we're used to really got started. Then B begat C and things were relatively quiet for a while, then boom! C++, Java, JavaScript, D, PHP, C#... – T.J. Crowder Aug 30 '15 at 07:20
  • @T.J.Crowder - Yes I understood that after Bergi's comment. I was thrown off because it looked like it was a well known / often used term but I couldn't find any references like that. – Amit Aug 30 '15 at 08:08
30

To my taste, a carefully structured nested ternary beats all those messy ifs and switches:

const isFoo = res.distance === 0;
const isBar = res.distance === 1 && res.difference > 3;
const isBaz = res.distance === 2 && res.difference > 5 && String(res.key).length > 5;

const word =
  isFoo ? 'a' :
  isBar ? 'b' :
  isBaz ? 'c' :
          'd' ;
Andrey Mikhaylov - lolmaus
  • 21,972
  • 5
  • 79
  • 126
  • +1 for giving name to the conditions, which makes the ternary operator more readable; then, there is no need to replace them. To answer the original question, you could include something like `// eslint-disable-next-line no-nested-ternary` – Ricardo Mar 16 '22 at 17:53
21

You could write an immediately invoked function expression to make it a little more readable:

const word = (() =>  {
  if (res.distance === 0) return 'a';
  if (res.distance === 1 && res.difference > 3) return 'b';
  if (res.distance === 2 && res.difference > 5 && String(res.key).length > 5) return 'c';
  return 'd';
})();

Link to repl

Yo Wakita
  • 4,962
  • 3
  • 21
  • 34
5

We can simplify it using basic operators like && and ||

let obj = {}

function checkWord (res) {
      return (res.distance === 0)   && 'a'
             || (res.distance === 1 && res.difference > 3) && 'b' 
             || (res.distance === 2 && res.difference > 5  && String(res.key).length > 5) && 'c'
             || 'd';
           
}

// case 1 pass
obj.distance = 0
console.log(checkWord(obj))

// case 2 pass
obj.distance = 1
obj.difference = 4
console.log(checkWord(obj))

// case 3 pass
obj.distance = 2
obj.difference = 6
obj.key = [1,2,3,4,5,6]
console.log(checkWord(obj))

// case 4 fail all cases
obj.distance = -1
console.log(checkWord(obj))
dhaker
  • 1,050
  • 12
  • 18
4

If you are looking to use const with a nested ternary expression, you can replace the ternary with a function expression.

const res = { distance: 1, difference: 5 };

const branch = (condition, ifTrue, ifFalse) => condition?ifTrue:ifFalse;
const word = branch(
  res.distance === 0,    // if
  'a',                   // then
  branch(                // else
    res.distance === 1 && res.difference > 3,   // if
    'b',                                        // then
    branch(                                     // else
      res.distance === 2 && res.difference > 5,   // if
      'c',                                        // then
      'd'                                         // else
    )
  )
);

console.log(word);

or using named parameters via destructuring...

const branch2 = function(branch) {
  return branch.if ? branch.then : branch.else;
}

const fizzbuzz = function(num) {
  return branch2({
    if: num % 3 === 0 && num % 5 === 0,
    then: 'fizzbuzz',
    else: branch2({
        if: num % 3 === 0,
        then: 'fizz',
        else: branch2({
          if: num % 5 === 0,
          then: 'buzz',
          else: num
        })
      })
  });
}

console.log(
  [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16].map(
    cv => fizzbuzz(cv)
  )
);

edit

It may be clearer to model it after the python if expression like this:

const res = { distance: 1, difference: 5 };

const maybe = def => ({
  if: expr => {
    if (expr) {
      return { else: () => def };
    } else {
      return { else: els => els };
    }
  }
});
const word = maybe('a').if(res.distance === 0).else(
  maybe('b').if(res.distance === 1 && res.difference > 3).else(
    maybe('c').if(res.distance === 2 && res.difference > 5).else('d')
  )
);
console.log(word);

edit

Another edit to remove the nested if/else branches:

const res = { distance: 1, difference: 5 };

const makeResolvedValue = def => {
  const elseProp = () => def;
  return function value() {
    return {
      if: () => ({ else: elseProp, value })
    };
  }
};

const value = def => ({
  if: expr => {
    if (expr) {
      return { else: () => def, value: makeResolvedValue(def) };
    } else {
      return { else: els => els, value };
    }
  }
});

// with branching if needed
const word = value('a').if(res.distance === 0)
  .else(value('b').if(res.distance === 1 && res.difference > 3)
    .else(value('c').if(res.distance === 2 && res.difference > 5)
      .else('d')
    )
  );
console.log(word)

// implicit else option for clarity
const word2 = value('a').if(res.distance === 0)
  .value('b').if(res.distance === 1 && res.difference > 3)
  .value('c').if(res.distance === 2 && res.difference > 5)
  .else('d');

console.log(word2);
Doug Coburn
  • 2,287
  • 23
  • 24
2

If all your truthy conditions evaluate to truthy values (so the value between the question mark and the semicolon evaluates to true if coerced to boolean...) you could make your ternary expressions return false as the falsy expression. Then you could chain them with the bitwise or (||) operator to test the next condition, until the last one where you return the default value.

In the example below, the "condsXXX" array represent the result of evaluating the conditions. "conds3rd" simulates the 3rd condition is true and "condsNone" simulates no condition is true. In a real life code, you'd have the conditions "inlined" in the assignment expression:

var conds3rd = [false, false, true];
var condsNone = [false, false, false];

var val3rd = (conds3rd[0] ? 1 : false) ||
  (conds3rd[1] ? 2 : false) ||
  (conds3rd[2] ? 3 : 4);

var valNone = (condsNone[0] ? 1 : false) ||
  (condsNone[1] ? 2 : false) ||
  (condsNone[2] ? 3 : 4);

alert(val3rd);
alert(valNone);

Your example could end up like below:

word = ((res.distance === 0) ? 'a' : false) ||
    ((res.distance === 1 && res.difference > 3) ? 'b' : false) ||
    ((res.distance === 2 && res.difference > 5 && String(res.key).length > 5) ? 'c' : 'd';

As a side note, I don't feel it's a good looking code, but it is quite close to using the pure ternary operator like you aspire to do...

Amit
  • 43,881
  • 8
  • 73
  • 106
  • Plus for being incredibly creative :) – dthree Aug 29 '15 at 21:25
  • @dthree - thanks. I see you "accepted" T.J.'s answer, and I guess that means you switched to using `if`s. May I ask why not the approach I suggested? I mean... it's *almost* identical to the one you started with, it even looks and indents in a similar fashion... – Amit Aug 29 '15 at 21:32
  • just because it crosses the line into unusual. Like, if anyone else looked at my code, it would probably be followed by obscene language. It's definitely creative, but it's enough extra complexity that it seems a bit much. – dthree Aug 30 '15 at 07:29
  • @dthree - I mostly agree, but it got me thinking... what if the code was a little more descriptive? What's your thought if a change is made as follows: `var __NEXT__ = false; word = ((x == 1) ? 'a' : __NEXT__) || ((x == 2) ? 'b' : __NEXT__) || ((x == 3) ? 'c' : 'd';` (And of course, `__NEXT__` could look a little different if you'd like...). Does that seem more plausible to you? – Amit Aug 30 '15 at 08:48
2
word = (res.distance === 0) ? 'a'
: (res.distance === 1 && res.difference > 3) ? 'b'
: (res.distance === 2 && res.difference > 5 && String(res.key).length > 5) ? 'c'
: 'd';

This is an older question, but this is how I would do it... I would start with the default case and then change the variable or pass it unchanged as desired.

var word = 'd';
word = (res.distance === 0) ? 'a' : word;
word = (res.distance === 1 && res.difference > 3) ? 'b' : word
word = (res.distance === 2 && res.difference > 5 && String(res.key).length > 5) ? 'c' : word;
K McCabe
  • 21
  • 1
  • In this case the conditions are mutually exclusive because of _res.distance_, but this doesn't work in general. If all conditions are true, word would become _[d,a,b,c]_, but the original ternary would keep it at 'a'. Even if you change the order to accomodate this, calculating conditions 2 and 3 might have side effects or be expensive, but in the ternary those are not executed if condition 1 is already truthy. – Wolfzoon Jun 01 '22 at 15:49
  • I didn't mean to imply that I'd always choose this method. If tests were expensive, then I would choose a switch. But that wasn't the question asked. The question was how to express the above logic without nested ternary operators or an if/else chain. The option of a switch was already covered, so I gave another option. You're right. All conditions being true in that the original logic would return 'a' and in my steps 'c'. Again, the question asked makes this impossible, but, if it's a concern, the order of the tests could be reversed to give 'a', IMHO, at a slight cost of readability. – K McCabe Jun 03 '22 at 01:22
2

Sometimes we have (or just love) to use one-line expressions or variable definitions. So, we can use a combination of destructive assignments with the ternary operator. For example,

was:

const a = props.a ?  props.a : cond2 ? 'val2.0' : 'val2.1' ;

let's update to:

const { a =  cond2 ? 'val2.0' : 'val2.1' } = props;

It even remains relatively well readable.

Alexei Zababurin
  • 766
  • 10
  • 13
1

If you use lodash you can use _.cond

Point free version with lodash/fp:

 const getWord = _.cond([
  [_.flow(_.get('distance'), _.eq(0)), _.constant('a')],
  [_.flow(_.get('distance'), _.eq(1)) && _.flow(_.get('difference'), _.gt(3)), _.constant('b')],
  [
    _.flow(_.get('distance'), _.eq(2))
    && _.flow(_.get('difference'), _.gt(5))
    && _.flow(_.get('key'), _.toString, _.gt(5)),
    _.constant('c'),
  ],
  [_.stubTrue, _.constant('d')],
]);
Mario Pérez Alarcón
  • 3,100
  • 2
  • 24
  • 36
1

If you're in the mood for something a little less readable... this might be for you. Write a general function to take an array of conditions (in the order you'd write your if/else) and an array of assignment values. Use .indexOf() to find the first truth in your conditions, and return the assignment array value at that index. Order is critical, conditions need to match up by index to the assignment you want:

const conditionalAssignment = (conditions, assignmentValues) => assignmentValues[conditions.indexOf(true)];

You can modify to handle truthy instead of struct true, and beware the undefined return if indexOf is -1

1

I personally love using ternary expressions for one liners. Although, I have to agree that nesting ternary expressions can lead to sketchy code.

I started playing with the Object constructor recently to write clearer code:

let param: "one" | "two" | "three";

// Before
let before: number = param === "one" ? 1 : param === "two" ? 2 : 3;

// After
let after: number = Object({
    one: 1,
    two: 2,
    three: 3
})[param];

Real life example:

const opacity =
    Platform.OS === "android"
      ? 1
      : Object({
          disabled: 0.3,
          pressed: 0.7,
          default: 1,
        })[(disabled && "disabled") || (pressed && "pressed") || "default"];
Almaju
  • 988
  • 10
  • 28
0

I faced this too recently and a google search led me here, and I want to share something I discovered recently regarding this:

a && b || c

is almost the same thing as

a ? b : c

as long as b is truthy. If b isn't truthy, you can work around it by using

!a && c || b

if c is truthy.

The first expression is evaluated as (a && b) || c as && has more priority than ||.

If a is truthy then a && b would evaluate to b if b is truthy, so the expression becomes b || c which evaluates to b if it is truthy, just like a ? b : c would if a is truthy, and if a is not truthy then the expression would evaluate to c as required.

Alternating between the && and || trick and ? and || in the layers of the statement tricks the no-nested-ternary eslint rule, which is pretty neat (although I would not recommend doing so unless there is no other way out).

A quick demonstration:

true ? false ? true : true ? false : true ? true ? true : false : true : true
// which is interpreted as
true ? (false ? true : (true ? false : (true ? (true ? true : false) : true))) : true
// now with the trick in alternate levels
true ? (false && true || (true ? false : (true && (true ? true : false) || true))) : true
// all of these evaluate to false btw

I actually cheated a bit by choosing an example where b is always truthy, but if you are just setting strings then this should work fine as even '0' is ironically truthy.

Ambyjkl
  • 430
  • 4
  • 15
  • can you please take a look at https://stackoverflow.com/questions/45508733/eslint-issue-with-refactoring-conditional-nested-if-else-statements I'm trying to find a way to avoid the nested ternary operator and I think your advice could work, but not sure exactly where the || should be used. – pixelwiz Aug 04 '17 at 15:07
0

I've been using a switch(true) statement for these cases. In my opinion this syntax feels slightly more elegant than nested if/else operators

switch (true) {
  case condition === true :
    //do it
    break;
  case otherCondition === true && soOn < 100 :
    // do that
    break;
}
ap-o
  • 170
  • 8
0

ES6 opens the door to this, a different take on a switch statement.

Object.entries({
  ['a']: res.distance === 0,
  ['b']: res.distance === 1 && res.difference > 3,
  ['c']: (res.distance === 2 && res.difference > 5 && String(res.key).length > 5) ? 'c'
}).filter(n => n[1] === true)[0][0]
dthree
  • 18,488
  • 11
  • 69
  • 103
0

Beside if-else and switch, you can also try square brackets.

const testName = cond1 ? [cond2 : a : b] : [cond3 ? c : d]
Yao Li
  • 1,791
  • 1
  • 22
  • 23