6 Useful Tips to write better Conditionals in JavaScript

6 Useful Tips to write better Conditionals in JavaScript

If you work with JavaScript, you would be writing a lot of code with many conditionals involved. The conditionals may appear easy to learn at first but there's more than writing a couple of if/else statements. Here are some useful tips to write better and cleaner code with conditionals.

In any programming language, the code needs to make decisions and execute actions depending on the condition given in the input.

For example, in a game, if the player's number of lives is 0, then it's game over. In a weather app, if it is being looked at in the morning, show a sunrise graphic; show stars and a moon if it is nighttime. In this article, we'll explore how so-called conditional statements work in JavaScript.

If you work with JavaScript, you would be writing a lot of code with many conditionals involved. The conditionals may appear easy to learn at first but there's more than writing a couple of if/else statements. Here are some useful tips to write better and cleaner code with conditionals.

Table of Contents

  1. Array.includes
  2. Early exit / Return early
  3. Object Literal or Map instead of Switch Statement
  4. Default Parameters and Destructuring
  5. Match all/partial criteria using Array.every & Array.some
  6. Use Optional Chaining and Nullish Coalescing

1. Array.includes

For multiple conditions use Array.includes

For example:

   function printAnimals(animal) {
   if (animal === 'dog' || animal === 'cat') {
      console.log(`I have a ${animal}`);
    }
   }

   console.log(printAnimals('dog')); // I have a dog

The above code looks good since we have only two animals to check. However, we're not sure of user input. What if, we get any other animal? If we will keep extending the statement with more OR statements, the code will get harder to maintain, and not that clean.

Solution:

We can rewrite the conditional above by using Array.includes

  function printAnimals(animal) {
   const animals = ['dog', 'cat', 'hamster', 'turtle']; 

   if (animals.includes(animal)) {
     console.log(`I have a ${animal}`);
   }
  }

  console.log(printAnimals('hamster')); // I have a hamster

Here, we have created an array of animals so that the conditions are extracted separately from the rest part of the code. Now, if we want to do a check for any other animal, all we need to do is add a new array item.

We can also use the animals variable out of the scope of this function to reuse it anywhere else in the code. This is a way to write cleaner code which is easier to understand and maintain. Isn't it?

2. Early exit / Return early

This is a very cool trick to condense your code and make it look cleaner. I remember when I started working professionally, I learned to write conditionals with early exit on my first day.

Let's take the previous example and add some more conditions. What if instead of the animal as a simple string, it's an object with certain properties.

So now the requirements are:

  • If no animal, throw an error
  • Print the type of animal
  • Print the name of the animal
  • Print the gender of the animal
const printAnimalDetails = animal => {
  let result; // declare a variable to store the final value

  // condition 1: check if animal has a value
  if (animal) {

    // condition 2: check if animal has a type property
    if (animal.type) {

      // condition 3: check if animal has a name property
      if (animal.name) {

        // condition 3: check if animal has a gender property
        if (animal.gender) {
          result = `${animal.name} is a ${animal.gender} ${animal.type};`;
        } else {
          result = "No animal gender";
        }
      } else {
        result = "No animal name";
      }
    } else {
      result = "No animal type";
    }
  } else {
    result = "No animal";
  }

  return result;
};

console.log(printAnimalDetails()); // 'No animal'

console.log(printAnimalDetails({ type: "dog", gender: "female" })); // 'No animal name'

console.log(printAnimalDetails({ type: "dog", name: "Lucy" })); // 'No animal gender'

console.log(
  printAnimalDetails({ type: "dog", name: "Lucy", gender: "female" })
); // 'Lucy is a female dog'

What do you think about the above code?

It works fine, but the code is long and difficult to maintain. One can waste some time figuring out where the closing brackets are if they don't use linting too. 😄 Imagine what would happen if the code has more complex logic. A lot of if..else statements!

We can refactor the above function with ternary operators, && conditions, etc. but instead let's write more precise code by using multiple return statements.

const printAnimalDetails = ({type, name, gender } = {}) => {
  if(!type) return 'No animal type';
  if(!name) return 'No animal name';
  if(!gender) return 'No animal gender';

  // Now in this line of code, we're sure that we have an animal with all the three properties here.

  return `${animal.name} is a ${animal.gender} ${animal.type}`;
}

console.log(printAnimalDetails()); // 'No animal'

console.log(printAnimalDetails({ type: dog })); // 'No animal name'

console.log(printAnimalDetails({ type: dog, gender: female })); // 'No animal name'

console.log(printAnimalDetails({ type: dog, name: 'Lucy', gender: 'female' })); // 'Lucy is a female dog'

In the refactored version, destructuring and default parameters is also included. The default parameter ensures that if we pass undefined as an argument to the method, we still have a value to destruct, here which is an empty object {}.

Usually, in the professional world, the code is written somewhere in-between these two approaches. Many people consider if...else statements easier to understand.

Another example:

  function printVegetablesWithQuantity(vegetable, quantity) {
  const vegetables = ['potato', 'cabbage', 'cauliflower', 'asparagus'];

  // condition 1: vegetable should be present
   if (vegetable) {
     // condition 2: must be one of the item from the list
     if (vegetables.includes(vegetable)) {
       console.log(`I like ${vegetable}`);

       // condition 3: must be large quantity
       if (quantity >= 10) {
         console.log('I have bought a large quantity');
       }
     }
   } else {
     throw new Error('No vegetable from the list!');
   }
 }

 printVegetablesWithQuantity(null); // error: No fruits
 printVegetablesWithQuantity('cabbage'); // print: red
 printVegetablesWithQuantity('cabbage', 20); 
 // 'I like cabbage`
 // 'I have bought a large quantity'

Now, look at the code above, we have:

  • 1 if/else statement that filters the invalid condition
  • 3 levels of nested if statements (condition 1, 2 & 3)

A general rule to follow is return early when invalid conditions found.

  function printVegetablesWithQuantity(vegetable, quantity) {

  const vegetables = ['potato', 'cabbage', 'cauliflower', 'asparagus'];

   // condition 1: throw error early
   if (!vegetable) throw new Error('No vegetable from the list!');

   // condition 2: must be in the list
   if (vegetables.includes(vegetable)) {
      console.log(`I like ${vegetable}`);

     // condition 3: must be a large quantity
      if (quantity >= 10) {
        console.log('I have bought a large quantity');
      }
   }
 }

By doing this, we have one less level of the nested statement. This coding style is good especially when you have long if statement.

We can further reduce the nesting ifs, by inverting the conditions & return early. Look at condition 2 below to see how we do it:

  function printVegetablesWithQuantity(vegetable, quantity) {

  const vegetables = ['potato', 'cabbage', 'cauliflower', 'asparagus'];

   if (!vegetable) throw new Error('No vegetable from the list!'); 
   // condition 1: throw error early

   if (!vegetables.includes(vegetable)) return; 
   // condition 2: return from the function is the vegetable is not in 
   the list 

  console.log(`I like ${vegetable}`);

  // condition 3: must be a large quantity
  if (quantity >= 10) {
      console.log('I have bought a large quantity');
  }
 }

By inverting the conditions of condition 2, the code doesn't have a nested statement anymore. This technique is useful when we have lot of conditions and we want to stop the further process when any particular condition is not met.

However, there's no hard rule for doing this. You can leave it as the previous version (condition 2 with nested), because:

The code is short and straight-forward, it is clearer with nested ifs. Inverting conditions may incur more thinking/cognitive load for the developers in the long run.

Therefore, always aim for Less Nesting and Return Early but don't overdo it.

3. Object Literal or Map instead of Switch Statement

Let's look at the example below, we want to print fruits based on color:

function printFruits(color) {
  // use switch case to find fruits by color
  switch (color) {
    case 'red':
      return ['apple', 'strawberry'];
    case 'yellow':
      return ['banana', 'pineapple'];
    case 'purple':
      return ['grape', 'plum'];
    default:
      return [];
  }
}

printFruits(null); // []
printFruits('yellow'); // ['banana', 'pineapple']

The above code is not wrong, but it's still quite verbose. The same result can be achieved with object literal with cleaner syntax:

// use object literal to find fruits by color
  const fruitColor = {
    red: ['apple', 'strawberry'],
    yellow: ['banana', 'pineapple'],
    purple: ['grape', 'plum']
  };

function printFruits(color) {
  return fruitColor[color] || [];
}

Alternatively, you may use Map to achieve the same result:

// use Map to find fruits by color
  const fruitColor = new Map()
    .set('red', ['apple', 'strawberry'])
    .set('yellow', ['banana', 'pineapple'])
    .set('purple', ['grape', 'plum']);

function printFruits(color) {
  return fruitColor.get(color) || [];
}

Map is the object type available since ES2015, which allows storing the key-value pair.

For the example above, the same result can be achieved with Array.filter as well.

 const fruits = [
    { name: 'apple', color: 'red' }, 
    { name: 'strawberry', color: 'red' }, 
    { name: 'banana', color: 'yellow' }, 
    { name: 'pineapple', color: 'yellow' }, 
    { name: 'grape', color: 'purple' }, 
    { name: 'plum', color: 'purple' }
];

function printFruits(color) {
  return fruits.filter(fruit => fruit.color === color);
}

4. Default Parameters and Destructuring

While working with JavaScript, we always need to check for null/undefined value and assign default value, or the compilation breaks.

  function printVegetablesWithQuantity(vegetable, quantity = 1) { // 
  if quantity has no value, assign 1

  if (!vegetable) return;
    console.log(`We have ${quantity} ${vegetable}!`);
  }

  //results
  printVegetablesWithQuantity('cabbage'); // We have 1 cabbage!
  printVegetablesWithQuantity('potato', 2); // We have 2 potato!

Much easier & intuitive isn't it?

What if vegetable is an object? Can we assign a default parameter?

  function printVegetableName(vegetable) { 
    if (vegetable && vegetable.name) {
     console.log (vegetable.name);
   } else {
    console.log('unknown');
   }
 }

 printVegetableName(undefined); // unknown
 printVegetableName({}); // unknown
 printVegetableName({ name: 'cabbage', quantity: 2 }); // cabbage

In the above example, we want to print the vegetable name if it's available or print unknown.

We can avoid the conditional if (vegetable && vegetable.name) {} by using default parameter & destructing.

  // destructing - get name property only
  // assign default empty object {}

  function printVegetableName({name} = {}) {
   console.log (name || 'unknown');
 }

 printVegetableName(undefined); // unknown
 printVegetableName({ }); // unknown
 printVegetableName({ name: 'cabbage', quantity: 2 }); // cabbage

Since we only need property name, we can destructure the parameter using { name }, then we can use name as a variable in our code instead of vegetable.name.

We also assign an empty object {} as a default value, otherwise it gives will an error when executing the line printVegetableName(undefined) - Cannot destructure property name of undefined or null, because there is no name property in undefined.

5. Match all/partial criteria using Array.every & Array.some

This tip is more about utilizing not so new Javascript Array methods to reduce the lines of code. Look at the code below, we want to check if all fruits are in red color:

const fruits = [
    { name: 'apple', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'grape', color: 'purple' }
  ];

function test() {
  let isAllRed = true;

  // condition: all fruits must be red
  for (let f of fruits) {
    if (!isAllRed) break;
    isAllRed = (f.color == 'red');
  }

  console.log(isAllRed); // false
}

The code is so long! We can reduce the number of lines with Array.every:

const fruits = [
    { name: 'apple', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'grape', color: 'purple' }
  ];

function test() {
  // condition: short way, all fruits must be red
  const isAllRed = fruits.every(f => f.color == 'red');

  console.log(isAllRed); // false
}

Much cleaner now right? In a similar way, if we want to test if any of the fruit is red, we can use Array.some to achieve it in one line.

const fruits = [
    { name: 'apple', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'grape', color: 'purple' }
];

function test() {
  // condition: if any fruit is red
  const isAnyRed = fruits.some(f => f.color == 'red');

  console.log(isAnyRed); // true
}

6. Use Optional Chaining and Nullish Coalescing

This part is still not much in use, but I want to include it in my tips for writing better conditionals. These two functionalities are a very useful addition to JavaScript.

At the moment of writing this, these options were not fully supported, and you needed to use Babel to compile the code.

Optional chaining enables us to handle tree-like structures without explicitly checking if the intermediate nodes exist, and nullish coalescing works great in combination with optional chaining and it's used to ensure the default value for an unexisting one.

Let's show some examples:

   const car = {
    model: 'Fiesta',
    manufacturer: {
    name: 'Ford',
    address: {
      street: 'Some Street Name',
      number: '5555',
      state: 'USA'
      }
    }
  } 

  // to get the car model
  const model = car && car.model || 'default model';

  // to get the manufacturer street
  const street = car && car.manufacturer && car.manufacturer.address && 
  car.manufacturer.address.street || 'default street';

  // request an un-existing property
  const phoneNumber = car && car.manufacturer && car.manufacturer.address 
  && car.manufacturer.phoneNumber;

  console.log(model) // 'Fiesta'
  console.log(street) // 'Some Street Name'
  console.log(phoneNumber) // undefined

So, if we wanted to print out if the car manufacturer is from the USA, the code would look something like this:

 const isManufacturerFromUSA = () => {
   if(car && car.manufacturer && car.manufacturer.address && 
 car.manufacturer.address.state === 'USA') {
     console.log('true');
   }
 }

 checkCarManufacturerState() // 'true'

You can see clearly how messy this can become in case of a more complex object structure. Many libraries, like lodash, for example, have their own functions as workarounds, but we don't want that, we want to be able to do it in vanilla js. Let's see a new way of doing things.

 // to get the car model
 const model = car?.model ?? 'default model';
 // to get the manufacturer street
 const street = car?.manufacturer?.address?.street ?? 'default street';

 // to check if the car manufacturer is from the USA
 const isManufacturerFromUSA = () => {
   if(car?.manufacturer?.address?.state === 'USA') {
     console.log('true');
   }
 }

This looks a lot prettier and easier to maintain. It's already in TC39 stage 3, let's wait for it to get approved and then we can see the use of this incredible syntax everywhere.

Summary

Let's learn and try new tips and techniques for writing more clean and maintainable code because, after a few months, long conditionals can be like shooting yourself in the foot. 😄

javascript

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

The essential JavaScript concepts that you should understand

The essential JavaScript concepts that you should understand - For successful developing and to pass a work interview

Data Types In JavaScript

JavaScript data types are kept easy. While JavaScript data types are mostly similar to other programming languages; some of its data types can be unique. Here, we’ll outline the data types of JavaScript.

JavaScript Memory Management System

The main goal of this article is help to readers to understand that how memory management system performs in JavaScript. I will use a shorthand such as GC which means Garbage Collection. When the browsers use Javascript, they need any memory location to store objects, functions, and all other things. Let’s deep in dive that how things going to work in GC.

Create a Line Through Effect with JavaScript

In this post we are going to create an amazing line through effect, with help of CSS and lots of JavaScript. So, head over to your terminal and create a folder LineThroughEffect. Create three files -index.html, main.js and styles.css inside it.

Grokking Call(), Apply() and Bind() Methods in JavaScript

In this article, we will have a look at the call(), apply() and bind() methods of JavaScript. Basically these 3 methods are used to control the invocation of the function.