57

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
  • 19,847
  • 14
  • 77
  • 106
  • 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

18 Answers18

42

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
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • @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
36

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
  • 23,107
  • 6
  • 84
  • 133
  • +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
  • 1
    Sometimes short-circuiting will be needed. If the isBar has this condition: `speed / res.distance > 42`, it will result to divide-by-zero error if res.distance is zero. To solve that, the named condition should be deferredly executed: `const isBar = () => speed / res.distance > 42` – Michael Buen Jul 04 '22 at 01:53
26

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
  • 5,322
  • 3
  • 24
  • 36
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,605
  • 18
  • 19
  • That would fail though if the value to be assigned is zero: `(res.distance === 0) && 0 || (res.distance === 1 && res.difference > 3) && 42` – Michael Buen Jul 04 '22 at 02:51
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,485
  • 27
  • 24
3

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
  • 45,440
  • 9
  • 78
  • 110
  • 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

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
  • 1,283
  • 12
  • 31
1

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
  • 927
  • 12
  • 15
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,468
  • 2
  • 27
  • 38
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

Here's an option that probably accomplishes the same thing, but might be verging on unreadable and liable to break. It's probably slightly less efficient than the ternary, too.

const word = [
  { value: 'a', when: () => distance === 0 },
  { value: 'b', when: () => distance === 1 && diff > 3 },
  { value: 'c', when: () => distance === 2 && diff > 5 && key.length > 5 },
  { value: 'd', when: () => 'otherwise' }, // just has to return something truthy
].find(({ when }) => when()).value

Array.prototype.find evaluates each element in the array until its callback returns a truthy value. Then it stops and returns, so we wouldn't evaluate any more cases than necessary.

We could also do it without a bunch of tiny functions. Slightly easier to read, but potentially less efficient if your calculations are slow or prone to throwing errors.

const word = [
  { value: 'a', when: distance === 0 },
  { value: 'b', when: distance === 1 && diff > 3 },
  { value: 'c', when: distance === 2 && diff > 5 && key.length > 5 },
  { value: 'd', when: 'otherwise' }, // just has to be something truthy
].find(({ when }) => when).value

But honestly, for shorter ternaries, I'd probably just go with an if/else (or remove the lint rule if you can get the rest of your team to agree).

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
  • 440
  • 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
  • 1
  • 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
  • 19,847
  • 14
  • 77
  • 106
0

I prefer inlined if-else in ternary expression

This..

let assignment;
if (loggedUser === onboard.Employee)
    assignment = AssignmentEnum.Employee;
else if (loggedUser === onboard.CreatedBy)
    assignment = AssignmentEnum.Manager;
else
    assignment = 0;

..can be shortened to:

const assignment = 
    loggedUser === onboard.Employee ?
        AssignmentEnum.Employee
    :loggedUser === onboard.CreatedBy ?
        AssignmentEnum.Manager 
    :
        0;

Just add the following ES6 lint rules to allow the above code structure

enter image description here

On "indent", add this rule:

"ignoredNodes": ["ConditionalExpression"]

On "operator-linebreak", add this rule:

{ "overrides": { "?": "after", ":": "ignore" } }

But if you don't want to put the assigned value in their own line, aside from the settings above, add the multiline-ternary to off

"multiline-ternary": "off"
Michael Buen
  • 38,643
  • 9
  • 94
  • 118
  • Please post code as text, not embed a painting of it. – Bergi Jun 17 '22 at 05:54
  • @Bergi Changed the code as text now. Left the eslint configuration as image, so as to see the diff of the specific configuration – Michael Buen Jun 17 '22 at 09:14
  • 1
    You might still want to post the two relevant lines as text, so that one could copy them into one's own configuration. – Bergi Jun 17 '22 at 14:18
  • Btw, I'm not sure your solution actually answers the question. The OP wants to *avoid* nested ternary expressions, not embrace them. – Bergi Jun 17 '22 at 14:18
  • @Bergi The OP embrace (`I personally loves ternary operators`) nested ternary expressions, it's just that their project's ESLint rules forbid it. Anyway, in his side projects, he can use the preferred style of code he posted, by using the settings I provided in my answer and by adding `"multiline-ternary": "off"`. I was searching for nested if-else in ternary, and the OP's post is on the top of the google search result, but none of the answers here provided the ESLint rules I'm looking for. And when I finally figured out the ESLint rules, I put the answer here so it can be googled later – Michael Buen Jun 18 '22 at 01:43
  • @Bergi `You might still want to post the two relevant lines as text, so that one could copy them into one's own configuration` Done – Michael Buen Jun 18 '22 at 01:51
0

I think some of the answers here kinda missed the point why the OP want to use ternary operator. Many prefers ternary expression as it allows them to DRY the assignment operations, not just because they can inline the returned value on same line as the condition and make the code have a lookup-like operation

If you deemed the switch(true) { is not ugly, I wish passing true is optional in javascript, either switch() { or switch {, then you can certainly DRY your assignment operations with switch:

const word = 
    (() => {
        switch (true) {
            case res.distance == 0:
                return 'a';
            case res.distance == 1 && res.difference > 3:
                return 'b';
            case res.distance == 2 && res.difference > 5 && String(res.key).length > 5:
                return 'c';
            default:
                return 'd';
        }
    })();
Michael Buen
  • 38,643
  • 9
  • 94
  • 118
-2

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

const testName = cond1 ? [cond2 : a : b] : [cond3 ? c : d]
Yao Li
  • 2,071
  • 1
  • 25
  • 24