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.
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?
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:
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:
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 if
s, 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 if
s.
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.
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);
}
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
.
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
}
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.
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