You don't need "if"

It was the first day in my last year of tech high school. The new programming teacher arrived and stood silent for a second, and then he started the lesson:

This year we will create a state machine with persistence using C++. This state machine will be a light-bulb that can be turned on, or off.

We all just look at each other thinking "ok, that will be easy"... and then he dropped the bomb:

There's a catch: You'll not be allowed to use if or for for it.

Now the class was clearly confused. Flow control is one of the first things we all learn as programmers. The main objective of that teacher was to teach us that we need to stop thinking conditions as if, repetitions as for, and so on, and instead be more abstract with the logic on our code. In JavaScript we have:

  • if/else.
  • for/of/in.
  • while.
  • do/while.
  • switch/case.
  • try/catch.

We will go through that list and learn about some of the alternatives we have, that from my point of view are generally safer and cleaner. Let's begin!

Conditions (if/switch)

Let's take this simple example as a starting point:

const welcomeMessage = ({ admin }) => {
	let message;
	if (admin) {
		message = "Welcome, administrator!";
	}
	return message;
};

So we have a function welcomeMessage which takes a user object and returns a message which depends on the user role. Now, because this if is quite simple, we might spot already that this has an issue, but JavaScript itself doesn't give us any kind of error. We don't have a default value for that message, so we need to do something like this:

const welcomeMessage = ({ admin }) => {
	let message = "Welcome, user";
	if (admin) {
		message = "Welcome, administrator!";
	}
	return message;
};

// Or

const welcomeMessage = ({ admin }) => {
	let message;
	if (admin) {
		message = "Welcome, administrator!";
	} else {
		message = "Welcome, user";
	}
	return message;
};

As I said in the introduction, we don't need if for this, we can use a ternary instead. A ternary has this shape:

boolean ? valueForTrue : valueForFalse

So we can change welcomeMessage to be like this:

const welcomeMessage = ({ admin }) =>
	admin ? "Welcome, administrator!" : "Welcome, user";

// Or

const welcomeMessage = ({ admin }) =>
	`Welcome, ${admin ? "administrator" : "user"}!`;

Ternaries have 3 advantages over ifs:

  1. They force us to cover all the logic branches (we are forced to have "else in all our ifs").
  2. They reduce the amount of code drastically (we just use a ? and a :).
  3. They force us to use conditional values instead of conditional blocks, which results in us moving logic from if blocks to their own functions.

The main argument against ternaries is that they become hard to read if we have several levels of nested ifs (ifs inside an ifs), and that's true, but I see that as yet another advantage. If you need to nest logic, that means that you need to move that logic away. So, let's have yet another example for this:

const welcomeMessage = ({ canMod, role }) =>
	`Welcome, ${
		canMod ? (role === ADMIN ? "administrator" : "moderator") : "user"
	}!`;

That became hard to read quite easily, but that means that we need to move some logic away from welcomeMessage, so we need to do something like this:

const roleText = role => (role === ADMIN ? "administrator" : "moderator");

const welcomeMessage = ({ canMod, role }) =>
	`Welcome, ${canMod ? roleText(role) : "user"}!`;

We covered if already, but what about switch? We can use a combination of plain objects and the ?? operator, so we go from this:

const welcomeMessage = ({ role }) => {
	switch (role) {
		case ADMIN:
			return "Welcome, administrator!";
		case MOD:
			return "Welcome, moderator!";
		default:
			return "Welcome, user!";
	}
};

To this:

const roleToText = role =>
	({
		[ADMIN]: "administrator",
		[MOD]: "moderator"
	}[role] ?? "user");

const welcomeMessage = ({ role }) => `Welcome, ${roleToText(role)}!`;

For those not familiar with the ?? operator, it works like this:

possiblyNullishValue ?? defaultValue

possiblyNullishValue can be either a value or "nullish" (null or undefined). If it is nullish, then we use defaultValue, if it isn't nullish then we use the value itself. Previous to this, we used to use ||, but that goes to the default for all falsy values (0, 0n, null, undefined, false, NaN and ""), and we don't want that.

Error handling (try/catch).

When we want to run something that might throw an error, we wrap it with a try/catch, as follows:

const safeJSONParse = value => {
	let parsed;
	try {
		parsed = JSON.parse(value);
	} catch {
		// Leave `parsed` `undefined` if parsing fails
	}
	return parsed;
};

const works = safeJSONParse("{}"); // {}
const fails = safeJSONParse(".."); // undefined

But we can get rid of that as well, using Promises. When you throw inside a promise, it goes to the catch handler automatically, so we can replace the code above with:

const safeJSONParse = value =>
	new Promise(resolve => resolve(JSON.parse(value)))
		// If it fails, just return undefined
		.catch(() => undefined);

safeJSONParse("{}").then(works => ({
	/* {} */
}));

safeJSONParse("..").then(fails => ({
	/* undefined */
}));

Or you can just use async/await and...

const works = await safeJSONParse("{}"); // {}
const fails = await safeJSONParse(".."); // undefined

Loops (for/while)

The for and while statements are used to loop over a "list" of things, but nowadays we have way better ways of doing that with the methods that come with some of those lists (arrays) or other functions that help us keep the same type of looping for objects as well. So let's start with the easiest, which is arrays:

const users = [
	{ name: "Luke", age: 32 },
	{ name: "Gandalf", age: 24_000 }
];

// Just logging
for (const { name, age } of users) {
	console.log(`The age of ${name} is ${age}`);
}

// Calculating average
let ageTotal = 0;
for (const { age } of users) {
	ageTotal += age;
}
console.log(`The average age is ${ageTotal / users.length}`);

// Generating new array from previous
const usersNextYear = [];
for (const { name, age } of users) {
	usersNextYear.push({ name, age: age + 1 });
}

Instead of using for for this, you can just use the Array.prototype.forEach for the logs, Array.prototype.reduce for the average and Array.prototype.map for creating a new array from the previous one:

// Just logging
users.forEach(({ name, age }) => console.log(`The age of ${name} is ${age}`));

// Calculating average
console.log(
	`The average age is ${users.reduce(
		(total, { age }, index, items) =>
			(total + age) / (index === items.length - 1 ? items.length : 1),
		0
	)}`
);

// Generating new array from previous
const usersNextYear = users.map(({ name, age }) => ({ name, age: age + 1 }));

There is an array method for pretty much everything you want to do with an array. Now, the "problems" start when we want to loop over objects:

const ages = {
	Luke: 32,
	Gandalf: 24_000
};

// Just logging
for (const name in ages) {
	console.log(`The age of ${name} is ${ages[name]}`);
}

// Calculating average
let ageTotal = 0;
let ageCount = 0;
for (const name in ages) {
	ageTotal += ages[name];
	ageCount += 1;
}
console.log(`The average age is ${ageTotal / ageCount}`);

// Generating new object from previous
const agesNextYear = {};
for (const name in ages) {
	agesNextYear[name] = ages[name] + 1;
}

I put the word "problem" between quotes because it was a problem before, but now we have great functions in Object: Object.entries and Object.fromEntries. Object.entries turns an object into an array of tuples, with the format [key, value], and Object.fromEntries takes an array of tuples with that format, and returns a new object. So we can use all the same methods we would use with arrays, but with objects, and then get an object back:

// Just logging
Object.entries(ages).forEach(([name, age]) =>
	console.log(`The age of ${name} is ${age}`)
);

// Calculating average
console.log(
	`The average age is ${Object.entries(ages).reduce(
		(total, [, age], index, entries) =>
			(total + age) / (index === entries.length - 1 ? entries.length : 1),
		0
	)}`
);

// Generating new object from previous
const agesNextYear = Object.fromEntries(
	Object.entries(ages).map(([name, age]) => [name, age + 1])
);

The most common argument about this approaches for loops is not against Array.prototype.map or Array.prototype.forEach (because we all agree those are better), but mainly against Array.prototype.reduce. I made a post on the topic in the past, but the short version would be: Just use whatever makes the code more readable for you and your teammates. If the reduce approach ends up being too verbose, you can also just do a similar approach to the one with for, but using Array.prototype.forEach instead:

let ageTotal = 0;
users.forEach(({ age }) => (ageTotal += age));
console.log(`The average age is ${ageTotal / users.length}`);

Edit: Improving readability

I knew I was forgetting something when I published the article, but the idea with the approach using array methods is also to move logic to functions, so let's take the last example of looping over objects and make it cleaner:

// If we will do several operations over an object, ideally we save the entries
// in a constant first...
const agesEntries = Object.entries(ages);

// We extract logic away into functions...
const logNameAndAge = ([name, age]) =>
	console.log(`The age of ${name} is ${age}`);

const valueAverage = (total, [, value], index, entries) =>
	(total + value) / (index === entries.length - 1 ? entries.length : 1);

const valuePlus1 = ([key, value]) => [key, value + 1];

// Now this line is readable...
agesEntries.forEach(logNameAndAge);

// Calculating average
console.log(`The average age is ${agesEntries.reduce(valueAverage, 0)}`);

// Generating new object from previous
const agesNextYear = Object.fromEntries(agesEntries.map(valuePlus1));

And not only more readable, but also now we have generic functionality that we can reuse such as the valueAverage or valuePlus1.

The other thing I forgot that usually replaces for and while is recursion (function that calls itself), but I don't usually use recursion myself. So, let's only do the sum of an array of numbers:

const sum = array =>
	array.length > 0 ? sum(array.slice(1)) + array[0] : 0;

sum takes an array, and calls itself until no array is left, adding the values in it and finally returning the total.

Closing thoughts

I want to emphasize something that usually gets lost in this series of articles I'm doing: The keyword in the title is NEED. I'm not saying you shouldn't use if/for/while and so on, I'm just saying that you might not need them, that you can code without them, and in some scenarios is even simpler (the majority of scenarios from my point of view). One of the names I considered for this series was "re-evaluating our defaults", because what I'm looking for is not to change 100% of your coding style, but actually to make you wonder:

Do I really NEED to do this, or is there a simpler way?

So, as usual, my final question for you is: Do you think you need if, for, while, and so on? Don't you think there might be a better way of solving that same issue with a simpler approach?

Thanks for reading this and if you disagree with something said in this post, just leave a comment and we can discuss it further.

See you in the next post of this series!

Disclaimer

This series is called "You don't need ...", emphasis on need, meaning that you would be fine without the thing that the post covers. This series explores alternatives, it doesn't impose them, so consider that before glancing over the post and ranting on the comment section. Keep it respectful.

Leave a comment