Tips and Tricks for Better JavaScript Conditionals and Match Criteria
Milos Protic
Posted on June 25, 2019
Originally posted on devinduct.com
Introduction
If you enjoy seeing the clean code, like me, you will try to write yours with as little conditional statements as possible. In general, object-oriented programming enables us to avoid conditionals and to replace them with polymorphism and inheritance. I believe we should follow these principles as much as we can.
As I mentioned in a different article, JavaScript Clean Code Best Practices, you are not writing the code just for the machine, you are writing it for the "future self" and for the "other guy".
On the other hand, due to various reasons, we might end up with conditionals in our code. Maybe we had a tight deadline to fix a bug, or not using a conditional statement would be too much of a change to our code base, etc. This article is written to get you through those cases and to help you organize the conditional statements you use.
Tips
Below are the tips about how you can structure the if...else
statements and how you can write less to achieve more. Enjoy!
1. First Things First, Trivial, but NOT Trivial
Don't use negative conditionals (they can be confusing) and use conditional shorthands for boolean
variables. I cannot stress this enough, especially the part about the negative conditionals. It's an unnatural way of doing things.
Bad
const isEmailNotVerified = (email) => {
// implementation
}
if (!isEmailNotVerified(email)) {
// do something...
}
if (isVerified === true) {
// do something...
}
Good
const isEmailVerified = (email) => {
// implementation
}
if (isEmailVerified(email)) {
// do something...
}
if (isVerified) {
// do something...
}
Now, when we have the above things clear, we can start.
2. For Multiple Conditions, use Array.includes
Let's say that we want to check if the car model is renault
or peugeot
in our function. The code might look something like this:
const checkCarModel = (model) => {
if(model === 'renault' || model === 'peugeot') {
console.log('model valid');
}
}
checkCarModel('renault'); // outputs 'model valid'
Considering that we have only two models, it might look acceptable to do it like that, but what if we want to do a check against another model? Or a couple of them more? If we add more or
statements the code will be harder to maintain and not that clean. In order to make it cleaner, we can rewrite the function to look like this:
const checkCarModel = (model) => {
if(['peugeot', 'renault'].includes(model)) {
console.log('model valid');
}
}
checkCarModel('renault'); // outputs 'model valid'
The code above looks nicer already. In order to make it even better, we can create a variable to hold the car models:
const checkCarModel = (model) => {
const models = ['peugeot', 'renault'];
if(models.includes(model)) {
console.log('model valid');
}
}
checkCarModel('renault'); // outputs 'model valid'
Now, if we want to do a check against more models, all we need to do is add a new array item. Also, if it was something important, we could declare the models
variable somewhere out of the scope of the function and reuse it wherever we need it. That way we centralize it and make maintenance a breeze, considering that we only need to change that one place in our code.
3. For Matching All Criteria use Array.every
or Array.find
In this example, we want to check if every car model is the one passed to our function. To achieve this in more imperative
manner, we would do something like this:
const cars = [
{ model: 'renault', year: 1956 },
{ model: 'peugeot', year: 1968 },
{ model: 'ford', year: 1977 }
];
const checkEveryModel = (model) => {
let isValid = true;
for (let car of cars) {
if (!isValid) {
break;
}
isValid = car.model === model;
}
return isValid;
}
console.log(checkEveryModel('renault')); // outputs false
If you prefer the imperative way of doing things, the code above might be fine. On the other hand, if you don't care what's going on under the hood, you can rewrite the function above and use Array.every
or Array.find
to achieve the same result.
const checkEveryModel = (model) => {
return cars.every(car => car.model === model);
}
console.log(checkEveryModel('renault')); // outputs false
By using Array.find
, with a little tweak we can achieve the same result, and the performance should be the same because both functions execute callback for each element in the array and return false
immediately if a falsy item is found.
const checkEveryModel = (model) => {
return cars.find(car => car.model !== model) === undefined;
}
console.log(checkEveryModel('renault')); // outputs false
4. For Matching Partial Criteria use Array.some
Like Array.every
does for all criteria, this method makes checking if our array contains one or more items pretty easy. To do it, we need to provide a callback and return a boolean value based on the criteria.
We could achieve the same result by writing a similar for...loop
statement like the one written above, but luckily we have cool JavaScript functions doing things for us.
const cars = [
{ model: 'renault', year: 1956 },
{ model: 'peugeot', year: 1968 },
{ model: 'ford', year: 1977 }
];
const checkForAnyModel = (model) => {
return cars.some(car => car.model === model);
}
console.log(checkForAnyModel('renault')); // outputs true
5. Return Early Instead of if...else
Branching
When I was a student, I was taught that a function should have only one return statement and that it should return from a single location only. This is not a bad approach if handled with care, meaning that we should recognize the situation when it would lead to conditional nesting hell. Multiple branches and if...else
nesting can be a pain if it goes out of control.
On the other hand, if the code base is big, and contains a lot of lines, a return statement somewhere in the deep would be a problem. Nowadays we practice separation of concerns and SOLID principles, therefore, a large number of code lines should be a rare occasion.
Let's create an example to illustrate this, and say that we want to display the model and manufacturing year of the given car.
const checkModel = (car) => {
let result; // first, we need to define a result value
// check if car exists
if(car) {
// check if car model exists
if (car.model) {
// check if car year exists
if(car.year) {
result = `Car model: ${car.model}; Manufacturing year: ${car.year};`;
} else {
result = 'No car year';
}
} else {
result = 'No car model'
}
} else {
result = 'No car';
}
return result; // our single return statement
}
console.log(checkModel()); // outputs 'No car'
console.log(checkModel({ year: 1988 })); // outputs 'No car model'
console.log(checkModel({ model: 'ford' })); // outputs 'No car year'
console.log(checkModel({ model: 'ford', year: 1988 })); // outputs 'Car model: ford; Manufacturing year: 1988;'
As you can see, the code above is pretty long even for this simple problem of ours. Imagine what would happen if we had more complex logic. A lot of if...else
statements.
We could refactor the function above in more steps making it better in each one. For example, use ternary operators, include &&
conditions, etc, but I will skip right to the very end and show you how, by using the modern JavaScript features and multiple return statements, it can be extremely simplified.
const checkModel = ({model, year} = {}) => {
if(!model && !year) return 'No car';
if(!model) return 'No car model';
if(!year) return 'No car year';
// here we are free to do whatever we want with the model or year
// we made sure that they exist
// no more checks required
// doSomething(model);
// doSomethingElse(year);
return `Car model: ${model}; Manufacturing year: ${year};`;
}
console.log(checkModel()); // outputs 'No car'
console.log(checkModel({ year: 1988 })); // outputs 'No car model'
console.log(checkModel({ model: 'ford' })); // outputs 'No car year'
console.log(checkModel({ model: 'ford', year: 1988 })); // outputs 'Car model: ford; Manufacturing year: 1988;'
In the refactored version, we included destructuring and default parameters. The default parameter will ensure that we have a value to destruct if we pass undefined
. Note that if we pass a null
value the function will throw an error and this is the advantage of the previous approach, because in that case, when null
is passed the output will be 'No car'
.
Object destructuring will ensure that the function gets only what it needs. For example, if we include additional property in the given car object it will not be available inside our function.
Depending on the preference, developers will follow one of these paths. The practice has shown me that, usually, the code is written somewhere in-between these two approaches. Many people consider if...else
statements easier to understand, which helps them to follow the program flow with less struggle.
6. Use Indexing or Maps Instead of switch
Statement
Let's say that we want to get car models based on the given state.
const getCarsByState = (state) => {
switch (state) {
case 'usa':
return ['Ford', 'Dodge'];
case 'france':
return ['Renault', 'Peugeot'];
case 'italy':
return ['Fiat'];
default:
return [];
}
}
console.log(getCarsByState()); // outputs []
console.log(getCarsByState('usa')); // outputs ['Ford', 'Dodge']
console.log(getCarsByState('italy')); // outputs ['Fiat']
The code above can be refactored to exclude the switch
statement totally.
const cars = new Map()
.set('usa', ['Ford', 'Dodge'])
.set('france', ['Renault', 'Peugeot'])
.set('italy', ['Fiat']);
const getCarsByState = (state) => {
return cars.get(state) || [];
}
console.log(getCarsByState()); // outputs []
console.log(getCarsByState('usa')); //outputs ['Ford', 'Dodge']
console.log(getCarsByState('italy')); // outputs ['Fiat']
Alternatively, we could create a class for each state with a list of available cars and use it when needed, but that is a topic for another post. This post is about conditionals. A more appropriate change would be to use an object literal.
const carState = {
usa: ['Ford', 'Dodge'],
france: ['Renault', 'Peugeot'],
italy: ['Fiat']
};
const getCarsByState = (state) => {
return carState[state] || [];
}
console.log(getCarsByState()); // outputs []
console.log(getCarsByState('usa')); // outputs ['Ford', 'Dodge']
console.log(getCarsByState('france')); // outputs ['Renault', 'Peugeot']
7. Use Optional Chaining and Nullish Coalescing
This section I can start by saying, "Finally". In my opinion, these two functionalities are a very useful addition to the JavaScript language. As a person coming from the C# world, I can say that I use these quite often.
At the moment of writing this, these options were not fully supported, and you needed to use Babel to compile the code written in such a manner. You can check the optional chaining here and the nullish coalescing here.
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 back up the statements above with some examples, and start with the old way of doing things.
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) // outputs 'Fiesta'
console.log(street) // outputs 'Some Street Name'
console.log(phoneNumber) // outputs undefined
So, if we wanted to print out if the car manufacturer is from the USA, the code would look something like this:
const checkCarManufacturerState = () => {
if(car && car.manufacturer && car.manufacturer.address && car.manufacturer.address.state === 'USA') {
console.log('Is from USA');
}
}
checkCarManufacturerState() // outputs 'Is from USA'
I don't need to tell you 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 checkCarManufacturerState = () => {
if(car?.manufacturer?.address?.state === 'USA') {
console.log('Is from USA');
}
}
This look a lot prettier and shorter, and to me, very logical. If you're wondering why should you use ??
instead of ||
, just think of what values can evaluate as true
or false
, and you will have a possible unintended output.
And one thing off topic, which is very neat. Optional chaining also supports the DOM API, which is very cool, meaning that you can do something like this:
const value = document.querySelector('input#user-name')?.value;
Conclusion
Ok, that's it what I have for now. If you liked the article, subscribe on devinduct.com (there is a small form at the end of each post :)) or follow me on twitter to stay tuned.
Thank you for reading and see you in the next article.
Posted on June 25, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.