JS tricks: avoiding if statements
Replacing nested conditionals with data
Introduction
This pattern probably has a well-known name, but the name is less important than the idea.
In a Telegram group, I saw someone ask:
I need to make this algorithm without using if. Could someone help me?
The algorithm needed to use sex, age, and employment time to determine whether someone could retire. Before I could respond, someone wrote it with a ternary expression. It was clever, but it only replaced one conditional form with another.
The situation reminded me of a BMI algorithm I wrote in college. It had to have low complexity and be easy to test because the course focused on testing. Everyone began with a quick implementation and ended up with deeply nested ifs. Stepping back before writing led to this approach:
- Create an enum for male and another for female, each containing the minimum and maximum index for that category.
- Receive the input and perform the calculation normally.
- With the result, search the enums for the range that satisfies the condition based on the calculated value.
In Java, this became fairly verbose—about six files for a relatively simple problem—but it met all the constraints.
The rest of this article applies the same idea in JavaScript and TypeScript.
Defining our cases
type Sex = "M" | "F";
type Measurements = { weight: number; height: number };
const titles = {
underweight: "Underweight",
normal: "Normal weight",
slightlyOverweight: "Slightly overweight",
overweight: "Above ideal weight",
obese: "Obesity",
};
// The values don't match reality, I studied IT not medicine.
// Consult your doctor and drink water
const parameters = {
M: [
{title: titles.underweight, min: 0, max: 20.7},
{title: titles.normal, min: 20.7, max: 26.4},
{title: titles.slightlyOverweight, min: 26.4, max: 27.8},
{title: titles.overweight, min: 27.8, max: 31.1},
// This case is absurd, but we need to cover everything above 31.1
{title: titles.obese, min: 31.1, max: Number.MAX_SAFE_INTEGER},
],
F: [
{title: titles.underweight, min: 0, max: 19.1},
{title: titles.normal, min: 19.1, max: 25.8},
{title: titles.slightlyOverweight, min: 25.8, max: 27.3},
{title: titles.overweight, min: 27.3, max: 32.3},
{title: titles.obese, min: 32.3, max: Number.MAX_SAFE_INTEGER},
],
};
const calculate = ({weight, height}: Measurements) => weight / height ** 2;
const sex = "M" as Sex; // input comes from user
const height = 1.8;
const weight = 77;
const bmi = calculate({height, weight});
const val = parameters[sex].find(
(measurement) => measurement.min <= bmi && bmi <= measurement.max
);
console.log("Status", val.title);
console.log("BMI", bmi);
This is the same algorithm I first wrote in Java, expressed in far fewer lines of JavaScript. Here is what each part does:
- Since it's in TS, first the type definitions
- Definition of titles in
titles - Here the parameters according to sex, we have a list of objects containing the title, the minimum BMI and the maximum BMI for each category.
calculatesays it all, it's our function that will calculate the BMI- From
sextobmithere's nothing different or striking. But here's a bonus, lately I've been opting to use parameters as objects, so I can name my received parameters in the function and avoid the confusion of positional parameters, especially when you don't have types - In
valwe do afindof the object in ourparameterslist to identify the title, minimum and maximum BMI - Finally, we display the values
That covers the core technique for avoiding nested ifs. For comparison, here is the equivalent implementation using conditional statements:
if (sex === "M") {
if (0 <= bmi && bmi <= 20.7) {
return {title: titles.underweight, min: 0, max: 20.7};
}
if (20.7 <= bmi && bmi <= 26.4) {
return {title: titles.normal, min: 20.7, max: 26.4};
}
if (26.4 <= bmi && bmi <= 27.8) {
return {title: titles.slightlyOverweight, min: 26.4, max: 27.8};
}
if (27.8 <= bmi && bmi <= 31.1) {
return {title: titles.overweight, min: 27.8, max: 31.1};
}
}
if (sex === "F") {
// the same nest of ifs
}
throw Error("Something wrong is not right");
Notice how much more complex this becomes. Adding new values or rules means adding more nested ifs, making the code increasingly difficult to maintain.
A natural question is: with multiple conditionals, how do we handle the case where no condition matches?
A valid point. The input does need to be guarded against invalid cases. A simple guard clause handles this cleanly:
if (!parameters.hasOwnProperty(sex)) {
throw Error("Something right is wrong");
}
// Normal flow of your program here
// It's nice to abort errors first
The guard clause completes the example and keeps the main flow focused on valid input.