Rethinking the JavaScript ternary operator
My name's James.
I work for a little startup called Atlassian.
And in my spare time, I also love to write about JavaScript.
Now I was doing some thinking a little while ago, and I was thinking about conditionals.
You see, I've been doing this coding thing for a while, and it seemed to me that over time I was using ternaries more, and if statements less.
And that seemed to be contrary to received wisdom.
See in my experience, people tend to treat ternaries as if they're dangerous or at the very least a little bit suspicious.
And you can find these attitudes encoded into linter rules, coding standards, and advice to new programs.
And they say things like don't nest ternaries and prefer if statements.
And so I began to wonder, am I doing it wrong?
What is it that's so wrong about ternaries?
So I did some reading and I wanted to work out where this suspicion was coming from.
I came across three main arguments about why ternaries are problematic.
First ternary operators are weird operators.
Second they're difficult for beginners.
And third they're difficult to read and nesting them gets quickly out of hand.
So we'll go through those one by one.
And the first thing is that they're weird.
So the ternaries operator is well an operator, but it's special.
That is in contrast to other kinds of operators.
See you're probably familiar with binary operators.
They're called binary operators because they operate on two expressions, one on the left side of the symbol and one on the right.
And this includes things like arithmetic operators, including plus, minus, times, divide, and also boolean operators like, and, and or, and we've got some fancy new operators recently, like nullish coalescing and optional chaining.
They're all examples of binary operators, but you're probably also familiar with unary operators.
They operate on just one expression and it's usually on the right, but not always.
And examples include the logical, not operator.
Or the unary negation operator that dash does double duty in JavaScript.
Now, binary and unary operators are familiar.
They're warm, they're cozy like a favorite blanket or a bedtime story.
But ternary operators, are weird because they operate on three expressions.
It has a condition and then two value expressions.
And as soon as we have three expressions, we need more than one symbol.
Otherwise we can't tell where the expression in the middle starts and ends.
So here's what one might look like in practice.
The actual ternary expression is that bit on the right of the equals.
If that request.secure condition is truthy, it evaluates to HTTPS.
Otherwise we get HTTP and we need two symbols, both the question mark, and the colon to keep that HTTPS separate from the condition and the else value.
So it has two symbols, three expressions, and that first expression is always cast to a boolean, but the others can be any type.
So it is a little odd and there's just one ternary operator.
So we often use the terms, conditional operator and ternary operator interchangeably.
And since there aren't any other ternary operators, it's kind of lonely and weird.
And given that weirdness, it's not surprising that people claim that it's difficult for beginners.
I mean, if you look at an if statement, you can mostly translate it into English.
Like this: if some condition is true, call takeAction else calls someOtherAction.
And it's those English words.
If and else that give you a clue about what's going on.
But with the ternary, it's nothing but cryptic, symbols.
I mean, question mark.
What's that?
Colon what's that doing?
Somehow you just have to know that those two symbols are connected.
In industrial design language we say there's no affordance there.
And speaking of knowing that those two symbols are connected somehow, what if you have more than one ternary how do you know which question mark belongs to which colon?
It's not always obvious.
And so you can see why people claim that they're difficult to read.
I mean, take a look at this beauty.
Those top three lines are just for context, but that bottom one is all one statement.
It doesn't even fit on a single line.
There's two question marks and two colns in there, and it's really not obvious at a glance, how they're paired.
And those symbols as cryptic enough to start with, but when you start nesting them things get hairy.
And to the point that some call them downright dangerous.
Because to be fair, readability matters.
The more difficult it is to work out what some piece of code is doing.
The easier it is to make a mistake and mistakes in comprehension lead to bugs in code and bugs in code cost, time, money, and sometimes even lives.
It's not a small thing for code to be difficult to.
read.
As Martin Fowler said, any fool can write code that a computer can understand.
Good programmers write code that humans can understand.
So we have three reasons why ternaries are problematic.
Ternaries are weird.
Ternaries can confuse beginners and ternaries are difficult to read, especially when you nest them.
Now I have a lot of sympathy for those objections, particularly the last two.
And if that's all there was to it, I'd tell everyone to drop ternaries.
Better to tell the junior programmer to stick with nice, safe, if statements.
Except are if statements safe?
Let's take a moment to dig a little further into this because you see if you look behind most of the objections to ternaries I think there's at least two assumptions going on here.
The first is the only reason you'd use a ternary is because you're trying to be brief or you're trying to be clever or both at the same time.
And the second assumption is that an if statement will do just as well in this place, and I've become convinced that neither of those assumptions is true.
This is because if statements and ternaries are different and not just a little bit different, they're different in a way that goes down to the fundamental building blocks of JavaScript.
To understand the difference we have to do some revision.
We have to go back to basics.
Now we need to understand the difference between a statement and an expression.
So what is that difference?
Well, in JavaScript an expression is something that resolves to a value.
So for example, one plus two is an expression that will resolve to the value three and a function call is an expression because the functions alway return a value.
Boolean comparisons are expressions that resolve to true or false and plain values are expressions too.
And the thing they all have in common is that you get a value somehow.
It might be undefined or null sometimes, but there's still a value you can pass around to do things with.
Now statements, on the other hand, don't resolve to a value.
Have a look at this example.
We have a while loop and there's stuff going on.
There's even expressions inside the loop.
But.
The result of calling the while loop doesn't give me back a value.
So expressions are things that resolve to a value, but a statement in contrast is what we call a standalone unit of execution.
Now you might be thinking well, so what, what's this got to do with anything?
Well, this matters because the ternary operator is an expression, but the, if statement is well, a statement and those two things are different and that difference matters.
Let's look at an example.
So here's an if statement and we define a variable, then depending on some condition, we assign it the result of either calculationA or calculationB.
And next the ternary.
Again, we define a variable then depending on some condition, we assign it the result of either calculationA or calculationB.
Now most people assume that those two pieces of code are equivalent.
And in one sense, they're right.
The way I described them, the description sounds the same.
In the end, our result variable will be assigned the result of calculationA or calculationB depending on some condition.
In another sense, though, those two examples are quite different.
And that changed from let to const gives us a clue as to why.
See our ternary is an expression.
And so the interpreter evaluates it, then assigns the result to that variable.
With the, if statement though something else is going on.
We first declare a variable without assigning it a value.
And then we choose to execute a sequence of instructions, which changes depending on some condition.
So we traveled down the first branch say and in that branch, we evaluate, calculate, A, then reach outside the if statement and mutate the result variable.
And that reaching outside and mutating is why if statements are shifty and not just if statements, it's all control statements, really, because the only way an if statement can do anything useful is by causing side effects.
It has no other option.
Now, what do I mean when I say, side effect.
Well, a side effect is anything the computer does besides calculating a value.
For example, making a network call.
Reading from the DOM.
Writing to a file.
Querying a database, logging to the console or mutating global variables.
These are all side effects.
And again, you might be thinking well, so what, who cares if they're side effects or not?
Those things they're the whole point of our program, they're the reason the code exists.
We write code to get stuff done.
And those side effects are the stuff that needs doing.
And of course that's true in one sense, it doesn't matter.
We're all just trying to write code that works.
That's what matters.
Right.
But the question is, how do you know your code works?
How do you know it's doing what you think it's doing?
How do you know its not dropping database tables?
How do you know it's not also mining doge coin?
How do you know it's safe.
And those questions-how do you know, is it safe?
These are the things that concern functional programmers and functional programers they want to have as much confidence as they possibly can, that the code is correct, and they will do whatever it takes to make that happen, including delving into the dark arts of mathematics.
So in functional programming, we treat side effects with great caution because that's where the uncertainty creeps in.
I mean, who knows if that database is going to be there when you try query it.
And who knows if someone's changed that global variable while we weren't looking.
So that's why if statements are a bit shifty because the only way if statements can do anything useful is with side effects.
Now, does this mean that we should never use if statements?
Well, no, but what it does mean is that we should treat them with an appropriate level of suspicion.
Every time you see one, you must ask yourself what side effect is happening here.
Because if you can't answer that question, you don't understand the code.
So, now that we understand if statements a little better, perhaps it's time to reconsider ternaries.
If if statements are so suspicious, a ternaries the solution then are they always better?
And the answer is no, but ternaries are nice because they're expressions and that means they're less suspicious when it comes to side effects.
But avoiding side effects, isn't the only reason we like working with expressions.
In functional programming, we try to work with expressions as much as possible.
In fact, I've heard at least one person define functional programming as coding with expressions.
And the reason functional programmers like expression so much is because they compose.
Operators and functions, they allow us to build complex expressions out of simple ones.
So for example, we can build complex strings with the concatenation operator like this one here.
And we could take that, uh, and we can pass it as a function argument, which gives us a new expression, which we can use with other operators.
And we can keep composing expressions with other expressions to make more expressions and build up some complex calculations.
And this is an excellent way to write code, but wait a minute, if we're just talking about building things out of other things, well, statements can do that too, right?
I mean, I can stick an if statement inside a for-loop and I can put a while loop inside my case switch statement.
And statements nest inside each other just fine.
So we can use statements to build other more complex statements.
What is it that's so special about expressions?
And it all comes down to inside versus out of, or as I like to put it Calico bags versus Lego bricks.
Let me explain.
So statements compose in the way that Calico shopping bags, compose.
They go inside each other.
And I can even do this very carefully.
I can for example, wrap my objects neatly in shopping bags, and I can stack them nicely on top of each other.
And I can put them all into one big, beautiful shopping bag.
And that's fine.
But there's no real structured relationship between the bags.
It doesn't really matter what you put inside the bags, so long as it fits and the things inside the bag don't really need to have any relationship to each other either.
There's no organizing principle that connects calico bags inside calico bags.
But with expressions, we say that we can make them out of other expressions.
And that gives us something called referential transparency.
Now, referential transparency is another thing you hear those functional programmers go on and on and on about.
But this referential transparency idea is what makes expressions similar to Lego bricks.
You see there's certain limitations in the way you can put Lego bricks together.
On your basic brick, there's nubs on the top that matched with gaps on the bottom of another brick and a whole system of matching nubs to gaps so that we can work other weirder shapes in as well.
But we can't just chuck any bricks together in any which way we want.
What we get when we put two bricks together is a shape.
For example, I could join two bricks together like this one, and that forms a new shape, but there's another way I could make that shape.
I could use three bricks instead of two.
What referential transparency means is that those two shapes are equivalent.
Anywhere I might want to use one, I could substitute the other and be absolutely certain it will work.
Now I'll admit it's not a perfect analogy, calico bags, and Lego bricks.
they're different.
They serve different purposes, but this idea of referential transparency is important.
So anywhere I have an expression, I can take that resulting value from that expression.
And I can use it where I would have put the original calculation.
So let's make this real.
Here we have our concatenated string from earlier.
Now let's imagine that it evaluates to this plain string here.
I can take this value and I can use it anywhere I might've used that other expression.
And I hope at this point, you're thinking, well, duh, of course, because it works exactly how we expect.
But what referential transparency means is it's a way of saying that that expectation is guaranteed by mathematics.
It will always work the way we expect 100% every single time.
And that my friends is gold because I want to be able to have that same confidence about my code.
I want to know that it's doing what I expect it to do 100% of the time guaranteed by mathematics.
Now in JavaScript, I can't quite guarantee everything as strongly as I might in other languages.
But what I try to do when I write my code is to write large chunks of it with expressions and no side effects.
And so I can least say this, this chunk right here, it will do what I expect every single time.
And that in turn is why I find myself using ternaries a lot, because if I use them carefully, they're trustworthy.
Now I have to give that caveat though about using them carefully because unfortunately, JavaScript doesn't really enforce that no-side-effects thing.
So it has no problem with something like this, which is sad because in spite of my best efforts, there's always a limit to how much I can trust my code.
What do we do then?
Do we write everything in pure script or rescript?
Well, perhaps, but that's often not an option.
So if we are using JavaScript, how do we get our jobs donesafely?
And the answer as usual is "it depends".
The best, most universally applicable advice I could give is use your discretion, consider your colleagues coding styles and preferences, take into account the specific cause of the problem you're trying to solve, weigh up the options and make a call.
All of which is about as helpful as saying, "write good code".
In the interest of actually being helpful, I'm going to give some advice, but I will do so with a massive caveat.
This is just my opinion.
Other people have different opinions and that is fine.
And the first piece of advice I have is understand that some statements are better than others.
So we've made the case that statements are a little bit shifty and we should try to stick with the expressions as much as possible.
But I don't know if you've ever tried that in JavaScript.
It doesn't really work.
See, the problem is that JavaScript code is mostly statements.
It's hard to write readable code without them.
So we need to use statements.
And fortunately, there are some statements that are safer than others.
The most dangerous statements are the ones with blocks.
They're the ones where you normally see curly braces.
Now that includes if statements, for loops, while loops, and switch case statements.
They're dangerous because the block never resolves to a value, and neither does the statement.
So the only way to do anything useful is by causing side effects.
Something has to reach outside that block scope and change the environment.
There are safer statements though.
They are variable assignments and return statements.
Now variable assignments are handy because they bind the result of an expression to a label.
Now we call this a variable and we can use the variable as an expression itself over and over again in other expressions.
So we like variable assignments because so long as we avoid mutation, they're relatively safe and they make our code more readable.
Return statements are also useful.
They make function calls resolve to a value.
Now that's valuable on its own, but they can do this anywhere inside the function body, including inside blocks so that we can use them to write safer if statements.
Let's dig in a little bit deeper on that.
To write safer if statements, I follow a simple rule.
The first block-that's the then bit-must always end with a return statement.
Now, ideally it wouldn't have anything but the return statement, but that's only if we're going for extra credit.
So what this does is it forces us to think very carefully about what goes inside an if statement.
We can't just open up a massive block and start firing off database requests and mutating variables all over the place.
Instead, we have to focus on things like guards, where we terminate early if some argument is invalid.
And you also have to keep your functions small and give them good names.
And the consequence of this rule is that you will never need an else block.
Not ever.
Consequently, if you do introduce an else block, you know that you've probably introduced a side effect.
Now it might be small and it might be benign, but it's likely still there.
And incidentally, this always returned rule works for switch case statements too.
But what about ternaries?
My advice for ternaries, is to keep them small.
Now that can be a challenge and it's not always feasible.
So when it's not feasible, vertical alignment can sometimes help.
So for example, here we have some deliberately bad, but real code that I that's from a charting library, I was working on.
It's attempting to work out what the Y axis of a chart should be.
Now with vertical alignment, we go from something like this, which is the way prettier would line things up.
to something like this.
Now it's still not great.
It's not a huge improvement, but at least to my eyes, I can see where that question mark colon pair lines up with all the equal signs.
I can see that they're related because they're aligned vertically.
Better approach though, would be to create some new variables to label the bits that go into the ternary.
So with our previous example, we start with these initial assignments.
We're trying to find the maximum and the minimum from an array of values.
And then we create four more variables to label the bits that go inside the ternary.
So first we're working out if the range is zero or not, then we work out a nice rounded range that's a factor of 10 for a nice, easy to read scale.
And we work out what to do if the range is zero.
And also we need an extra condition for what to do if the only value is zero.
And then we put it all together in one final ternary.
The trouble here is we're doing a calculation we don't need to.
We only need to calculate the default range if the range is empty and we only need to calculate the rounded range if the range is not empty.
Now we could spend a lot of time debating the evils of premature optimization, but let's assume for the moment that those calculations are indeed expensive.
So we can delay their calculation by putting them inside a function like I've done here.
And then in the ternary we only call the one we need.
But what about nesting ternaries?
Isn't that always bad?
Well, not really so long as you're keeping them small.
And if you're prepared to be brave and disable prettier and use vertical alignment.
The thing to remember is that you just need to always keep the nesting in the else position, that is the last expression in the ternary.
So nesting is particularly nice for situations where you have something like a lookup.
Some people would prefer to format it this way, but I don't think it matters.
And note that in both examples, I've had to disable prettier to make things line up.
So it is possible to use both ternaries and if statements responsibly, but it will take some effort.
And we also have to defy linters and formatters and coding standards on top of all of that.
And yes, sometimes people will put too much into ternaries and make something unreadable either because they're lazy or because they're in a hurry or because they just don't know any better.
But I think that's still better than blindly assuming that if statements are safe.
Now you might be thinking well, is that the best that we can do?
I mean, both ternaries and if statements have serious drawbacks, but conditionals, theyre bread and butter stuff.
And there is some hope.
There's a stage one, TC 39 proposal called do expressions.
Now what this does is it lets us, let's us wrap up some statements in a block prefixed with the keyword `do`.
It then takes the last evaluated value inside the statement and resolves the do expression to that value.
In other words, we can take any statement and we can turn it into an expression.
So for example, we could do something like this, which is like our protocol example from earlier, but quoite readable.
And as many people have pointed out, this can be particularly handy in react components because inside JSX, you can only have expressions, no statements, but do expressions would allow us to do something like the following.
So here I have some code that renders a different username if the user is an academic.
So with the do expression, I can run a transformation on an array inside a JSX expression and assign that to a variable, which is something I couldn't normally do.
Now, whether this is a good idea or not is open to debate, but it certainly gives us more options.
Now, unfortunately, it's only stage one, so it's not in browsers yet, but there is a Bable transform for it.
So you can start using it if you want to give it a go.
So let's revise what we've looked at.
To sum up.
It's pretty common for people to treat ternaries with suspicion.
They might suggest never using them at all, or at least never nesting them.
And instead you should probably use if statements.
I want to challenge this idea that your statements are actually safer, though.
It's important to understand that if statements are statements, not expressions and that has consequences for your code.
So next time you're doing a code review and you see a ternary well maybe consider whether the person might've had a good reason for putting it there.
And with that, I'll stop rabbiting on and let you get on with the rest of the conference.
Thank you.