Improve your Javascript conditionals

Writing conditionals are unavoidable when building software. It is one of the key topics when learning Javascript. However, conditionals are also the biggest contributor to push our software into entropy. It is important to be clear and explicit with our conditionals to ensure the quality of our code is maintained.

Modern Javascript provide us a vast arsenal of tools and methods to help structure our code. Here are some tips to improve your Javascript conditionals:

1. Name your condition

The first and maybe the most impactful thing you can do to improve your code is naming things correctly, that includes when dealing with conditionals. After all,

There are only two hard things in Computer Science: cache invalidation and naming things.

  • Phil Karlton

Be explicit with what you're checking for when writing an if statement, especially so if its a multi-condition check. You may get a pass if its a short one liner with single condition.

BAD
if (item.group === 'fruits' && item.name === 'apple' && item.status === 'ripe') {
  // slice my apple
}
GOOD
const isAppleRipe = item.group === 'fruits' && item.name === 'apple' && item.status === 'ripe';

if (isAppleRipe) {
  // slice my apple
}

Another quick tip here: naming boolean type variable, start it with "is", or "should" or "has", that are intuitively of boolean nature.

2. Type coercion

Javascript is a dynamically typed language, that means variable in Javascript can switch data type on the fly if you're not careful. Understand what are truthy and falsy values, and type cast your conditional check into boolean using ! and !! (single and double exclamation). This is especially useful when writing Typescript that are type sensitive when return values.

const list = [];

const isEmpty = !list.length;
const isNotEmpty = !!list.length;

In most cases, you want to avoid explicitly checking for type. Ultimately it depends on the requirement, maybe in certain cases you want to be really explicit that you're checking for undefined, or null, but usually you can get away with simply casting them into boolean.

if (!isAvailable) {
  // do your thing
}

// instead of

if (isAvailable === null || isAvailable === undefined || isAvailable === false) {
  // do your thing
}

If you're using Typescript, you should instead leverage on it's capability by declaring variable type along side with the variable. Otherwise, default parameters in javascript can also help your case here. Design your software in a way where falsy or truthy value is expected.

BAD
function doYourThing = (isActive) => {
  if (isActive !== null || isActive !== undefined || isActive !== false) {
    ...
  }
}
GOOD
function doYourThing = (isActive: boolean = false) => {
  if (isActive) {
    ...
  }
}

In cases where we'd check for object property value, we're used to a long list of condition chaining, ie. item && item.name to avoid nullish reference error. We can now use optional chaining when checking for object property, which would return undefined if it is not available.

BAD
if (item && item.name) { ... }
GOOD
if (!!item?.name) { ... }

3. Guard clause

Guard clause is a fancy way to tell you to always return early. Write your conditions to exit a function, rather than conditions to enter a function, to put it succinctly:

BAD
function getUserSession(user) {
  if (!!user.name && !!user.password) {
    // proceed to get user session
  }
}
GOOD
function getUserSession(user) {
  if (!user.name || !user.password) {
    return;
  }

  // proceed to get user session
}

By exiting a function early with guard clause, you get the invalid cases out of the way first (the bouncer pattern), before proceeding with the "real" body of your function. This will effectively reduce your code indentation caused by multi-level nested if else statements that are hard to read and hard to edit.

Mentally it also helps your fellow developers to jump to the next function earlier without needing to read through the whole body of function.

4. Conditional operators

A lot of the times we need to assign values or call a function based on condition. We can use conditional operators to make our code a little cleaner and easier to follow. Which conditional operator to use will depend on the requirement.

When declaring a variable, we should minimize the need for re-assignment to help with predictability of your code. In the same spirit, it is good to prefer const when declaring a variable.

In cases where the value of the variable can be different if condition is true or false, we can use ternary operator to help shorten our code:

BAD
let itemGroup;

if (itemType === 'apple') {
  itemGroup = 'fruit';
} else {
  itemGroup = 'vegetable';
}
GOOD
const itemGroup = itemType === 'apple' ? 'fruit' : 'vegetable';

However, be mindful to not overdo it with multiple nested ternary operator. In cases where that is required, consider restructuring your logic or use one of the methods mentioned later in the article to handle conditionals with multiple outs.

In cases where we need to assign value if value is falsy, we can use OR || operator. If we want to be more explicit with our checking, to target only null or undefined we can use nullish coalescing operator.

const itemNameFalsyCheck = item.name || 'Name is falsy';

const itemNameNullOrUndefinedCheck = item.name ?? 'Name is null or undefined';

5. List check

There are many scenarios where we are dealing with a list of values, and we'll want to check for something in the list, either if value exist, or value is of certain type, etc.

If we're checking for multiple values we can use Array method .includes or Set method .has instead of chaining multiple OR operators.

BAD
const isFruit = (item.type === 'apple' || item.type === 'orange' || item.type === 'durian');

if (isFruit) { ... }
GOOD
const isFruit = ['apple', 'orange', 'durian'].includes(item.type);

// or

const isFruit = new Set(['apple', 'orange', 'durian']).has(item.type);

It is worth nothing that Set.has has a significant performance edge over Array.includes, especially when dealing with a large data set, it is worth fitting Set checking in your code when possible.

However in cases where Set has to be repeatedly re-initialized (ie. in a loop), the cost of initializing Set will be too big, the offset will result in worse performance, and in such cases it is better to stick with Array.includes.

BAD
const listOfFruits = list.filter(x => {
  const fruits = new Set(['apple', 'orange', 'durian']);
  return fruits.has(x);
});
GOOD
const listOfFruits = list.filter(x => ['apple', 'orange', 'durian'].includes(x));

If we're checking for any of the items in a list is of certain value, we can use Array.some:

const hasFruits = list.some(x => x.type === 'fruit');

If we're checking whether all of the items in a list if of certain value, we can use Array.every:

const itsAllFruits = list.every(x => x.type === 'fruit');

6. Multiple outs

In cases where we are checking for more than 2 outs, there are multiple ways to handle them without the need for multiple if else statement. It is in fact best to avoid multiple if else as it is notoriously hard to read, hard to edit, and also slower in performance in comparison to the few options that we have. They are namely, switch statements, object literals, and Map.

BAD
let itemGroup;

if (item.name === 'apple') {
  itemGroup = 'fruit';
} else if (item.name === 'carrot') {
  itemGroup = 'vegetable';
} else if (item.name === 'mazda') {
  itemGroup = 'car';
} else {
  itemGroup = 'fruit';
}
GOOD
let itemGroup;

// with switch
switch (item.name) {
  case 'carrot':
    itemGroup = 'vegetable';
    return;
  case 'mazda':
    itemGroup = 'car';
    return;
  case 'apple':
  default:
    itemGroup = 'fruit';
    return;
}

// with object
itemGroup = {
  apple: 'fruit',
  carrot: 'vegetable',
  mazda: 'car'
}[item.type] ?? 'fruit';

// with Map
itemGroup = Map()
  .set('apple', 'fruit')
  .set('carrot', 'vegetable')
  .set('mazda', 'car')
  .get(item.type) ?? 'fruit';

We can also use IIFE with switch to make our switch statement much more readable. Using IIFE to handle switch cases also open up your conditional check for data massaging before returning. Just be mindful and not overdo it, keep it small and simple.

const itemGroup = ((type) => {
  case 'carrot':
    return 'vegetable';
  case 'mazda':
    return 'car';
  case 'apple':
  default:
    return 'fruit';
})(item.type);

Conclusion

Be mindful when writing conditionals in your code. It is one of the foundation to becoming a solid Javascript developer. One last tip I have is to extract your commonly used check / validations into small utility functions. They are easily readable, testable, which will result in a less error prone system. That is all that I have to share when it comes to improving your Javascript conditionals. Happy coding!

28