Refactoring cascading conditionals in favor of readability
Matt Angelosanto
Posted on June 23, 2021
Written by Juan Cruz Martinez ✏️
JavaScript is an extremely flexible programming language used to build pretty much anything you can think of, from websites, web applications, and desktop applications, to UIs for smart devices, server-side applications, and more.
JavaScript’s flexibility is what has allowed for its broad feature set — but, as we know, it is also responsible for some strange behaviors that have ignited developers' imaginations. Some of the code we’ve written is uniquely suited to solving these strange problems in smart and elegant ways; some isn’t.
In this article, we’ll focus on analyzing the different ways developers have replaced wordy and confusing conditional statements — in particular, cascading if/else if
and switch
statements. Why? Because in JS, we can do better than just using if
.
Ternary, &&
, and ||
operators
Let’s introduce a simple function with a conditional statement using if
, and let’s refactor it using the ternary operator.
if (condition) {
return functionTrue();
} else {
return functionFalse();
}
There’s nothing wrong with our example above, but we do unnecessarily take up a few lines of code repeating the keyword return
. The ternary operator allows for simplification:
return condition ? functionTrue() : functionFalse();
Isn’t that much simpler? But how does it work?
The ternary operator is the only JavaScript operator that takes three operands: a condition followed by a question mark (?
), an expression for a truthy conditional followed by a colon (:
), and finally an expression for a falsy conditional. Here is what it looks like:
condition ? expressionIfTrue : expressionIfFalse
Note that both true
and false
expressions must be provided for the ternary operator to work. But what if we only need to do something when the condition is truthy?
JavaScript offers alternative methods of simplifying expressions by making use of the operators &&
and ||
.
Let’s look at a different example where we only need to execute a statement when the condition is satisfied.
if (condition) {
console.log("it's true!");
}
We could rewrite this statement into a one-liner by using &&
, like so:
condition && console.log("it's true!");
The key reason this works is that JavaScript reads the operands in conditional statements from left to right and exits the moment it can invalidate the arguments. So, in the case of &&
, if the first statement is falsy, there’s no point in evaluating the next, as the whole expression is falsy.
Similarly, the ||
operator will continue evaluating the operands until one of them is true
, or the whole expression evaluates to false
. Take a look at the below example:
trueCondition || console.log("Hello world!"); // does not execute the console.log
falseCondition || console.log("Hello world!"); // executes the console.log
Evaluating multiple results for an expression
Often, when we’re reading or writing code, we find multiple nested if
conditions — such as in the following function, which takes the name of a fruit and returns its color.
function getColor(fruit) {
if (fruit.toLowerCase() === 'apple') {
return 'red';
} else if (fruit.toLowerCase() === 'banana') {
return 'yellow';
} if (fruit.toLowerCase() === 'orange') {
return 'orange';
} if (fruit.toLowerCase() === 'blueberry') {
return 'blue';
} if (fruit.toLowerCase() === 'lime') {
return 'green';
}
return 'unknown';
}
Even when the code performs its function as expected, there are several things we could do better. For starters, the method toLowerCase
is being called multiple times for each fruit, which could not only affect performance but also make the whole function less readable.
The next optimization would be to avoid repeating the conditionals, which reduces the number of instances we could introduce errors, such as forgetting to include the toLowerCase
method in one of our lines.
We can quickly fix this by calling the method only once at the beginning of the function and evaluating each result — but we can do even better by using a switch
statement.
function getColor(fruit) {
switch(fruit.toLowerCase()) {
case 'apple':
return 'red';
case 'banana':
return 'yellow';
case 'orange':
return 'orange';
case 'blueberry':
return 'blue';
case 'lime':
return 'green';
default:
return 'unknown';
}
}
This is looking much better, but it still doesn’t feel right. There are a lot of repeated keywords, which make it confusing to read.
Below is a different approach — a smarter, more elegant approach like we discussed at the beginning of this article.
function getColor(fruit) {
const fruits = {
'apple': 'red',
'banana': 'yellow',
'orange': 'orange',
'blueberry': 'blue',
'lime': 'green',
};
return fruits[fruit.toLowerCase()] || 'unknown';
}
Simply beautiful. It’s easy to identify which fruit corresponds with each color, we aren’t repeating keywords, and it’s clearly read and understood.
This method for solving cascading if
statements is called Jump Table. It can work for much more than simple texts or constants; let’s see a more complex example.
Building Map objects
The Jump Table approach is great for simple texts and constants, but how would it work in more complex situations, like when if
statements have multiple lines of codes with function calls?
Now that we understand how to simplify statements, the approach for these more complex scenarios is straightforward — it’s all about how we build our Map object.
Let’s build a calculate
function with two numbers and an operation as an argument, and return the operation’s result over the two numbers.
function calculate(number1, number2, operation) {
const operations = {
'+': (a, b) => a + b,
'-': (a, b) => a - b,
'*': (a, b) => a * b,
'/': (a, b) => a / b,
}
return operations[operation]?.(number1, number2) ?? 'invalid operation';
}
As expected, the code looks very clean and a function is clearly assigned to each operation to perform the necessary calculations to get our desired result.
What looks a bit different, and perhaps strange, is the return
statement; but the idea behind it is simple, so let’s break it down.
operations[operation]?.(number1, number2)
The first part of the expression will simply return the given operation from the dictionary and execute the function if the key is present. If the key does not exist, it will return undefined
. This last part is thanks to the optional chaining operator.
The second part uses the nullish coalescing operator, which returns its right-hand side operand when its left-hand side operand is null
or undefined
and otherwise returns its left-hand side operand.
?? 'invalid operation';
So, in our case, it will return the result of the operation when the operation is present in the dictionary, or it will return an invalid operation
.
Conclusion
JavaScript is a flexible language that offers multiple ways to solve a problem. In this article, we learned various alternatives to traditional if
statements that can help you write clearer, better code.
It’s important to have multiple options in your code arsenal because there is no single solution that will be right for every situation. Additionally, JavaScript is evolving, and new ways will be introduced or discovered as new versions roll out, so it’s helpful to stay connected and read the latest articles to stay up to date.
Thanks for reading!
LogRocket: Debug JavaScript errors easier by understanding the context
Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.
LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Posted on June 23, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024