DEV Community

Cover image for You don't need "if"
LUKESHIRU for Vangware

Posted on • Updated on

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"}!`;
Enter fullscreen mode Exit fullscreen mode

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"
    }!`;
Enter fullscreen mode Exit fullscreen mode

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"}!`;
Enter fullscreen mode Exit fullscreen mode

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!";
    }
};
Enter fullscreen mode Exit fullscreen mode

To this:

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

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

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

possiblyNullishValue ?? defaultValue
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 */
}));
Enter fullscreen mode Exit fullscreen mode

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

const works = await safeJSONParse("{}"); // {}
const fails = await safeJSONParse(".."); // undefined
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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 }));
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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])
);
Enter fullscreen mode Exit fullscreen mode

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}`);
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (68)

Collapse
ogranada profile image
Andres Granada

Interesting article, definitely it shows a different way to think, but, I think we need to have in mind that readability is not about how long is the code or how many sentences we have in a file, is about structure and experience. As an example, if you’re working with a junior dev maybe a ternary operator is not the best option to make the code readable for him, and in many cases the compiler/transpiler/interpreter transform the if and the ternary operator in an if.

Collapse
lukeshiru profile image
LUKESHIRU Author • Edited on

A few things:

  1. About the readability, you're right! I completely forgot to show how it looks like when you move stuff to functions with clear names, so you go from array.map(/* a bunch of code here inline */) to something like array.map(double) which is way more readable. I update the article with a new section that I forgot from my notes here (new section "Edit: Improving readability").
  2. You can teach a junior to read a ternary in 5 minutes, is far from being a hard concept.
  3. Minifiers might turn ifs into ternaries or shortcircuits, but the idea here is to show the power it has to use them oneself. If you don't write the else for an if, the minifier might turn it into condition?value:undefined, which isn't ideal. Ideally you should change that undefined, unless you intentionally wanted to return a "nullish" value.

Thanks for reading!

Edit: Just to add to this, the post wasn't even pointed at juniors, it's set to 7 which is "almost expert", so yeah, this is more of a post for folks that are "stuck in their ways".

Screenshot showing the level of this post set to 7

Collapse
andreidascalu profile image
Andrei Dascalu

Just a note: readability isn’t about understanding the concept or not. If a dev doesn’t know how to read a ternary at all, that’s a blocker. Readability is about how fast you understand the code by glancing at it. If a dev needs to pause to decipher a ternary ( maybe a multi level one), that’s a readability issue.
Code doesn’t need to be clever, it needs to be maintainable.

Thread Thread
lukeshiru profile image
LUKESHIRU Author

That's a pretty common complaint about ternaries, but I'll argue that this:

condition ? "true value" : "false value";
Enter fullscreen mode Exit fullscreen mode

Is a readable (or maybe even more) than this:

if (condition) {
    return "true value";
} else {
    return "false value";
}
Enter fullscreen mode Exit fullscreen mode

I learnt ternaries when I was learning programming, and it was a pretty easy concept to grasp (for me and my classmates as well). Usually I see this "ternaries are hard to read for juniors", but the thing is that if becomes hard to read, that's a sign that you need to move some functionality away to its own function or reconsider part of your logic (as I mentioned in the article, I see that as a pro). If you find that you can't think conditions without thinking in the if keyword, that might be a problem. The idea is that you should be able to think logic branches as actual branches and not as "if/else" blocks. Don't get me wrong, I don't support stuff like nesting ternaries inside ternaries or anything like that that will become quite cryptic in no time, but as I mentioned in the article, ternaries have some advantages over ifs that from my point of view are pretty valuable.

As you said, devs shouldn’t need to pause to decipher code in general, my point is that when using ternaries, if you need to stop to decipher, that only means that you need to move that code to a function with a clear name so we can abstract that "blockage" away.

Thanks for reading and commenting!

Thread Thread
andreidascalu profile image
Andrei Dascalu

well, for a large part readability is in the eye of the beholder and such decisions should be taken based on team discussions and clear metrics.

But, three small details, just for the sake of the discussion:

if (condition) {
    return "true value";
} else {
    return "false value";
}
Enter fullscreen mode Exit fullscreen mode

-> as a nitpick, you don't need an else at all. You have return statements.

  1. if 's can also be taken in separate functions to the same effect with the added bonus that you'd never need to use else in that situation, you'd just return.

  2. The top advantage of if is that in most languages you don't re-introduce other symbols and/or change their meaning. if (condition) { is a pretty clear statement using the same block separators. That block may have as many or as few lines as needed, which isn't the case of a ternary that's meant to be used only in the simplest of cases. So then why have a double standard at all? if statements are also portable between languages, while ternaries can be different (if they exist at all). Just look at PHP and how many different versions of ternaries it has. Fewer lines of code doesn't always mean more readable. Depending on context it can have the opposite effect.

Granted, I'd use a ternary in one case (and one alone), to replace

if (condition) {
   myVar = "xxxx";
} else {
   myVar = "yyyy";
}
Enter fullscreen mode Exit fullscreen mode

to

myVar = (condition) ? "xxxx" : "yyyy";
Enter fullscreen mode Exit fullscreen mode

though I'd still ask myself if it's worth creating a double standard and slightly increase cognitive load since I can also improve my code with

myVar = "yyyy";

if (condition) {
  myVar = "xxxx";
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
vforteli profile image
Verner Fortelius • Edited on

I would agree about the readability, but I think the biggest benefit of a ternary in cases like these is being able to use const directly

Thread Thread
lukeshiru profile image
LUKESHIRU Author

readability is in the eye of the beholder

100% agree. For me, for example, the early return approach is less readable, I always try to keep my functions with a single point of output. When I see a block like:

if (condition) return "true value";
return "false value";
Enter fullscreen mode Exit fullscreen mode

I read "If condition then return "true value", return "false value"" ... I know that the early return will break the function so the "false value" woundl't run, but is just less readable from my point of view than just doing:

condition ? "true value" : "false value"
Enter fullscreen mode Exit fullscreen mode

Which I read as "condition? Then "true value". Else "false value"". I actually would prefer an if/else over an early return.

The top advantage of if is that in most languages you don't re-introduce other symbols and/or change their meaning.

I mean, ternaries are pretty much everywhere, but even if you come from a language that doesn't have them, is a pretty easy concept to understand.

That block may have as many or as few lines as needed, which isn't the case of a ternary that's meant to be used only in the simplest of cases.

I mentioned this on the article, but I see that as yet another advantage of ternaries, mainly because if you need more logic in that condition, then you need to move that logic away to its own function. You can do this with ifs as well, but is not "enforced" by the language, you just have to agree with your whole team to try to keep conditions simple and move complex branches to their own functions.

Fewer lines of code doesn't always mean more readable. Depending on context it can have the opposite effect.

I agree, that's why in the past I wrote an article talking about the importance of naming. I love functional programming, but for example hate how Haskell uses single letters for everything, making it short but extremely and unnecessarily hard to read. My idea is that if you move logic away to keep your ternaries short, those functions need to have significant names that make the content of that logic branch obvious without having to go into the function to see what it does.

Thanks for adding comments like this one! Actually showing more examples makes it way more useful for everyone!

Collapse
pro465 profile image
Proloy Mishra

amazing article, although id argue that if there are multiple nested ternaries snd you separate them in yheir own functions, it might require the dev to jump around the code, which might make it less readable

Collapse
lukeshiru profile image
LUKESHIRU Author

Maybe, yes. But my point is mainly that you should name those functions in a way that doesn't require the dev to jump to it, that the implementation is obvious without having to go take a look at it.

Collapse
pro465 profile image
Proloy Mishra

hmmm...and what if we actually have no fitting name for an intermediate effect? or the concern of the code becoming "too broken"?

Thread Thread
lukeshiru profile image
LUKESHIRU Author

It should always be possible to name a "branch" in logic, mainly because you're doing something in one branch, and something else on other, so those actions have names. But I understand your concern about the concern being "too broken", because it might feel like the logic is "all over the place". The thing is, that kinda forces us to something good that is "keeping it simple", if something is too complex, maybe we should consider making that logic a little bit simpler? Maybe this function is doing "way too much"? I tend to keep my functions below 10 lines of code (or even 5). This makes testing and maintenance way easier because every function does "one thing and does it well".

Thread Thread
pro465 profile image
Proloy Mishra • Edited on

yeah, but what if the logic is actually complex with no way of simplifying it?

btw thanks for the informative post!

Thread Thread
lukeshiru profile image
LUKESHIRU Author • Edited on

Our work as devs could be summarized in that: "yeah, but what if the logic is actually complex with no way of simplifying it?" 🤣 ... yup, in that case if you have something super complex and not easy to either split or simplify, then go ahead and use an if until you find a better way. If you keep those if isolated to complex issues only, then your code will be far easier to maintain, and when you want to work on "debt", is easy to find, just look in your codebase for those if that you weren't able to simplify before 😄

Thread Thread
pro465 profile image
Proloy Mishra • Edited on

now thats a good advice, thanks again!

Collapse
lukeshiru profile image
LUKESHIRU Author • Edited on

You're right! I completely forgot to show how it looks like when you move stuff to functions with clear names, so you go from array.map(/* a bunch of code here inline */) to something like array.map(double) which is way more readable. I update the article with a new section that I forgot from my notes here (new section "Edit: Improving readability"). Thanks for reading!

Collapse
gr3kidd3r profile image
Masked Man

I don't know you personally and take my words as no offense, but the better developer you get the less you show off in your code with little hacks and obscure language features (They're fine in coding competitions though).
I defenitely am a fan of using some of those in simple cases like:

const message = isAdmin ? "Hello Admin" : "Hello user";
Enter fullscreen mode Exit fullscreen mode

over

let message = "Hello user";
if (isAdmin) {
  message = "Hello Admin";
}
Enter fullscreen mode Exit fullscreen mode

but not in all situations.

Collapse
lukeshiru profile image
LUKESHIRU Author • Edited on

Is definitively not for all situations. The post point is to avoid using only if when we talk about conditions, only for/while when we talk about looping, and so on, and instead think logically in a pure way, instead of thinking in a "syntactic" way.

Now about what you said:

but the better developer you get the less you show off in your code with little hacks and obscure language features

I agree, but I don't see how promises, ternaries or array methods are "obscure" or "hacky" in any way. From my pont of view is way more hacky when folks do stuff like in this comment where they use + to do type coercion, or when they do short-circuits like condition && trueResult instead of just using a ternary, and so on. But using ternaries, promises and array methods is just using tools that the language actually provided.

Collapse
gr3kidd3r profile image
Masked Man

By definition hack means using something in a clever way that was not (at least at first intended) to use.
So using Promise which has a very definitive and clear usage out of its context as an exception handling mechanism seems hacky to me and may be to others as well.
Next point is, programming (specially in productive environments) should be treated as verse as opposed to poem in a sense that you use words for their exact meanings rather than metaphors or implications or so. In that context when you see something like if you're 100% sure that you're dealing with a conditional, or be it a try/catch for exception handling; But when you use promise for that matter the struggle begins.

Thread Thread
lukeshiru profile image
LUKESHIRU Author

I beg to differ. A Promise is a proxy for a value not necessarily known when the promise is created. When you "try" something, is because that something might fail, so you don't know how it will go. That's why when you throw inside a Promise, you go to the .catch of that promise. If I had to write it like this...

new Promise((resolve, reject) => {
    try {
        const parsed = JSON.parse(value);
        resolve(parsed);
    } catch (error) {
        reject(error);
    }
});
Enter fullscreen mode Exit fullscreen mode

...I would agree with you, but the implementation is way simpler because Promises deal with throws automatically by rejecting the promise. Using a promise for that is not being hacky from my point of view. Heck, we can even say there is not much difference between saying "I'll try to get that value" and "I promise I'll get that value". "Promise" sounds more committed, but functionally we deal with them the same way, that's why async/await was so easily implemented with try/catch, because it works pretty much the same way.

I agree with you that "programming should be treated as verse as opposed to poem", but sticking to one way of doing stuff like for example only using if when we are thinking of conditions or for/while for loops is very limiting to the way you'll express your logic in code.

A few examples:

// lots of boilerplate
for (const user of users) {
    greet(user);
}

// vs. this, that almost reads as a
// sentence with weird punctuation
users.forEach(greet);

// or
// again losts of boilerplate
if (loggedIn) {
    return "Hello, user!";
} else {
    return "Please, login";
}

// vs. this, that again almost reads as a sentence
loggedIn ? "Hello, user!" : "Please, login";

// or
// try/catch with await, lots of boilerplate again...
try {
    const response = await fetch(API);
    const data = await response.json();
    console.log(data);
} catch (error) {
    console.error(error);
}

// vs...
fetch(API)
    .then(response => response.json())
    .then(console.log)
    .catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Shorter code doesn't always mean you're being "clever", sometimes it only means you're getting rid of boilerplate code to keep it concise but still readable.

Thread Thread
gr3kidd3r profile image
Masked Man

I will not be a nitpicker anymore but just to clarify, Promise is not executed on the program's main thread and has its own task queue so it's not exactly the same as try/catch and you're expanding its definition; Although that might be a good choice in a specific scenario.

In your next point you're comparing two different paradigms of coding (procedural and somehow functional) which are not the exact same thing and has some different mechanisms for expressing different things. I'll stick to use the right one for the right context, mixing them won't help readability.

Collapse
naupathia999 profile image
Lauren Pagan

"You don't need if logic, just use the syntactically different if logic instead!"

"You don't need to iterate! just use the language's iterator functions instead!"

I guess I got clickbaited pretty hard for this article. Kudos.

Can't believe kids these days

Collapse
lukeshiru profile image
LUKESHIRU Author

You missed the point. The idea is that we have more ways of thinking in conditions than if, or in iterations than for or while. Those aren't the only tools we have, and we should avoid thinking in conditions as ifs or iterations as fors and try to think it in a more "abstract" way.

I would close with something like "boomers this days", but I don't assume age based on coding style. I saw people 30+ years old and coding with the latest trends, and I saw 20- years old folks coding with jQuery and PHP. Age means nothing, just how open you are to learn new stuff.

Cheers!

Collapse
cgatian profile image
Chaz Gatian

Is it actually more readable? I guess that's up to the reader. I personally don't think it's more readable, and really struggle with these multi operation one liners.

I think the author of the code may think it's more readable because they are on their second or third iteration of the code, refactoring and condensing. To them it's more readable because they've looked at it for so long it actually does become more readable! But for someone just seeing the code for the first time, sometimes it's not that clear.

I'd take your advice lightly. The fact that you felt compelled to write an article to show people how to remove if statements might be a signal that not everyone is as versed as you might be in this style, and it's better to code in a way that reaches the largest audience.

If you compare the Angular codebase vs Webpack codebase you will see two different styles. One definitely is more favorable to open source. I'll let you judge which one it is as you read the code :).

Collapse
lukeshiru profile image
LUKESHIRU Author

The fact that you felt compelled to write an article to show people how to remove if statements.

That's not what the article is about. Is just to show that if is not the only way of writing conditions, and for/while is not the only way of writing iterations.

I mentioned already several times in the article and in comments that if the code stops being readable, is a sign that you need to move some logic away to its own function with a clear and concise name. The idea with going with a less verbose version of the code isn't only to make it shorter, but similar to english when you use a word like "explicit" instead of writing "clearly stated", you create new "words" (functions) that represent longer "sentences" so your "text" is still clear yet less verbose.

Collapse
baenencalin profile image
Calin Baenen

Why do administrators get punctuation, but users don:t?
Are we not good enough, admins?

const welcomeMessage = ({ admin }) => {
    let message = "Welcome, user"; // Add dot (.)
    if (admin) {
        message = "Welcome, administrator!";
    }
    return message;
};
Enter fullscreen mode Exit fullscreen mode
Collapse
lukeshiru profile image
LUKESHIRU Author

lol

Collapse
mmikael_18 profile image
Matti Uusitalo

I use most of these constantly but often i don't too and the reason is the readability of the code deteriorates when it is too compact.

Collapse
lukeshiru profile image
LUKESHIRU Author • Edited on

That actually depends on how you write it. In the examples I completely forgot to show how it looks like when you move stuff to functions with clear names, so you go from array.map(/* a bunch of code here inline */) to something like array.map(double) which is way more readable. I update the article with a new section that I forgot from my notes here (new section "Edit: Improving readability"). Thanks for reading!

Collapse
duridah profile image
durid-ah

I agree, things can go south pretty quickly. I've seen my fair share of nested ternary statements at work

Collapse
lukeshiru profile image
LUKESHIRU Author

Nested ternaries are as bad practice as nested ifs are. I cover this in the 5th snippet in the article (the last ternary snippet, just before the switch alternative snippets), but basically, if you need to nest logic, that usually means that you need to move logic away to it's own function.

Collapse
deathshadow60 profile image
Info Comment hidden by post author - thread only accessible via permalink
deathshadow60

So many of your alternatives are either painfully cryptic, almost as if it's got that TLDR nose-breather attitude behind it. Tale your CASE example. Which one is clearer more comprehensible code? Certainly doesn't help with JS object lookups being so slow it's also slower executing.

Your "for" vs "Array.forEach" example being a classic "how inefficient and how many unnecessary execution steps can we add" -- for ZERO improvement in code legibility. Just as the CONST in your loop helps card-stack its performance by adding extra checks to the process. Why would you add the overhead of a function call to each and every iteration of the loop?!? That's what Array.forEach does.

Because guess what? for..of is the NEWER of the two... and was added BECAUSE the overhead of callbacks for each and every iteration can be painfully wasteful.

The function call overhead being why your "lets make a function for every joe blasted line of code" allegedly "improved" version is bloated, inefficient, and HARDER to work with, not easier.

Collapse
lukeshiru profile image
LUKESHIRU Author • Edited on

I promised myself I wouldn't answer unrespectful comments, but is kinda puzzling that you seriously find this:

array.forEach(log)
Enter fullscreen mode Exit fullscreen mode

Has zero gains in legibility over this:

for (var i; i < array.length; i++) {
    log(array[i]);
}
Enter fullscreen mode Exit fullscreen mode

Or this:

for (const item of array) {
    log(item);
}
Enter fullscreen mode Exit fullscreen mode

When the first one literally reads "array for each log", vs. the second one that reads "for i, while it's smaller than the length of array, add 1 to it and log array[i]", or the third one that reads "for item of array, log item".

Or you find the switch vs. object approach "cryptic", when I'm not even the only one doing this ... but ok.

Collapse
deathshadow60 profile image
deathshadow60

No disrespect was meant to the individual, just to the work... which I found to be the opposite of the claims,

Though nice attempt at card-stacking the most out of date version of doing that.

for (var item of array) log(item);

Which has BETER clarity than forEach, without the overhead of the callback functions. That's why for..of was introduced AFTER Array.foreach. Don't add extra steps inside the loops that you don't have to.

Thread Thread
toxictoast profile image
Comment marked as low quality/non-constructive by the community. View Code of Conduct
ToxicToast

I am punshing myself for admitting it, but @deathshadow60 is absolutly right...
for...of is way better (readwise and performancewise) than array.foreach or the for loop crap you seem to like...

Thread Thread
lukeshiru profile image
LUKESHIRU Author • Edited on

No need to call "crap" something you don't like. And you aren't taking the full picture into consideration, is not only forEach, we also have map, filter and so many other more idiomatic/readable ways of dealing with loops than for...of:

const doubles = numberArray.map(double);

// vs

const doubles = [];
for (const number of numberArray) {
    doubles.push(double(number));
}

// ...

const evens = numberArray.filter(isEven);

// vs

const evens = [];
for (const number of numberArray) {
    if (isEven(number)) {
        evens.push(number);
    }
}
Enter fullscreen mode Exit fullscreen mode

And we can go on. And the performance ... welp ... that depends on the engine I guess, but in Chrome using methods can be faster

Collapse
lexlohr profile image
Alex Lohr

There's another interesting pattern you can use, by combining an array with the knowledge that false will be coerced to the number zero and true to one, e.g.

const roleName = ['normal user', 'administrator'][+admin];
Enter fullscreen mode Exit fullscreen mode
Collapse
lukeshiru profile image
LUKESHIRU Author

I usually avoid "hacky" code such as short-circuiting and coercion. I'll literally prefer this because of readability:

const roleName = ['normal user', 'administrator'][admin ? 0 : 1];
Enter fullscreen mode Exit fullscreen mode

Because we are being more "clear" about our intention. Not to mention that if we add more roles, this will become worse. Nevertheless this is interesting, thanks for sharing!

Collapse
lexlohr profile image
Alex Lohr

I wouldn't consider manual coercion "hacky", unlike automatic coercion. It's a normal language feature of weak typed languages. The problem are a lot of programmers who believe the language should behave differently than it is specified.

Thread Thread
lukeshiru profile image
LUKESHIRU Author

You're doing "automatic" coercion when you write +boolean, because the + operator is automatically turning that boolean into number. Ideally you should use something like parseInt, parseFloat or Number if you want to coerce manually in a clear way.

Collapse
sguertin profile image
Scott Guertin

There was a time when the size of your JS files could make or break the experience of a web site, these days it's not typically a problem though. So I'd say reducing the "size" of the code isn't actually worthwhile if it doesn't improve readability as well. Any code base that has more than one contributor should be conscientious about the readability over performance so long as the performance is "good enough".

Collapse
lukeshiru profile image
LUKESHIRU Author

The point of the post is just to be less verbose and keep our logic concise. The point of the post is not about optimizing the size of the bundle, even if I disagree with you about that not being a problem nowadays. There are lots of folks saying "internet is good enough now" and forgetting not only folks around the world, but also that even if you have the best mobile data, there are places where the internet is flaky and you'll notice the extra kb in a Webapp (subways, rural zones, some condos, and so on).

Collapse
sguertin profile image
Scott Guertin • Edited on

That's what minifiers are for, a far better solution than deliberately harming readability in favor of conciseness in the code base.

Thread Thread
lukeshiru profile image
LUKESHIRU Author • Edited on

Still missing the point of the post. The idea is not to "minify" the code, or write it in a short version just to write it in a short version ... the idea is to write it in a short version if that makes the code easier to read, for example you wouldn't do this:

if (condition) {
  return "something";
} else {
  return "something else";
}
Enter fullscreen mode Exit fullscreen mode

When you could just:

return condition ? "something" : "something else";
Enter fullscreen mode Exit fullscreen mode

Is shorter, but clearer. At the same time you shouldn't do this:

+string;
Enter fullscreen mode Exit fullscreen mode

When you can do this:

parseInt(string, 10);
Enter fullscreen mode Exit fullscreen mode

Basically the idea is not "write it short", but "write it with less boilerplate if that helps making the code clearer or cleaner".

Collapse
merelj profile image
merelj

Whoa.
Some interesting thoughts in this one.

I sure can agree that array methods are a more readable option then loops (tradeoffs being comparative slowness and need to chain filter/map/reduce every time you need to implement something somewhat complex; but, hey, I'd say that is justified in most cases).

I can also agree that sometimes it's easier to read ternaries then "if"s (100% so when it is a simple variable assignment or some code within JSX; although you WILL have problems scaling that code for more complex logic)

BUT

creating an entire object worth of data and using a single value out of it just to avoid using a switch sounds like a bad idea when you consider performance. it might work fine for simple objects with primitives as values and just a few fields, but anything larger than that -- and you're hogging memory and considerably slowing down your code.

and as for wrapping your sync functions with promises to "simplify" error handling... I have got to say that this really seems like an absolutely terrible way to go: you're basically forcing yourself to either turn every single function in an async one or return to the promise hell that was so unintuitive to work with that async/await was introduced almost immediately. this sure does not look like a simplification to me -- not to mention performance hit it will absolutely lead to.

all in all, I think the "improvements" you are suggesting are mostly ok for smaller projects and might even sometimes help with readability, but have some serious drawbacks if you're working on large scale projects or care about your app performance.

Collapse
lukeshiru profile image
LUKESHIRU Author

you WILL have problems scaling that code for more complex logic

I covered this on the post and come comments, but if you need complex logic on a ternary, that means that you need to move that logic away, avoid inlining it.

creating an entire object worth of data and using a single value out of it just to avoid using a switch sounds like a bad idea when you consider performance. it might work fine for simple objects with primitives as values and just a few fields, but anything larger than that -- and you're hogging memory and considerably slowing down your code.

Not quite, the idea with using "dictionaries" (objects with value => value mapping) instead of switch/case is to write them once and reuse them as needed (which also becomes super useful when working with i18n). About performance you can check it here.

you're basically forcing yourself to either turn every single function in an async one or return to the promise hell that was so unintuitive to work with that async/await was introduced almost immediately. this sure does not look like a simplification to me -- not to mention performance hit it will absolutely lead to.

First, IDK about you, but for me at least this:

try {
    console.log(JSON.parse(value));
} catch (error) {
    console.error(error);
}
Enter fullscreen mode Exit fullscreen mode

Is way more "hellish" than this:

new Promise(resolve => resolve(JSON.parse(value)))
    .then(console.log)
    .catch(console.error);
Enter fullscreen mode Exit fullscreen mode

But still, the main point with the post is to show that there isn't a single way of doing stuff, but you can use stuff like promises instead of try/catch which is way closer to the Maybe approach of functional languages. Performance might be worse (it depends, in this example, is better to use promises), but that's to be expected because you're creating a new instance of Promise and gaining a better API to deal with that operation that "could fail", not to mention that said API can be used with other "promised" operations. So you could chain the JSON parser of the example with a Promise.all with a few fetch requests, and maybe even with node:fs/promises, without having to go from one API to the other (a mix of try/catch with .then/.catch). As everything mentioned in this article, is something to consider in some scenarios, but it doesn't mean that you should use it always, just when it makes sense.

all in all, I think the "improvements" you are suggesting are mostly ok for smaller projects and might even sometimes help with readability, but have some serious drawbacks if you're working on large scale projects or care about your app performance.

Is not so much "improvements", is more like "alternatives". The important thing is not to default to one or the other. I use several of these techniques in large projects, and as a team, we had some cases where we had to evaluate if it was worth a ~3% performance improvement over having a code that would be harder to read and maintain, and we chose to keep it with the functional approach. The only time I actually had a performance issue, the problem was that the back-end was sending 10.000 elements to the front-end without any type of pagination whatsoever, so the front-end was struggling both loading that amount of data, and showing it into the DOM, but that was quickly fixed in the back-end because it wasn't a front-end responsibility at all.

Thanks for taking the time to read the article and comment!

Collapse
qm3ster profile image
Mihail Malo

The performance in the linked case is worse.
You just weren't waiting for the operation to execute.
See this fixed example

Thread Thread
lukeshiru profile image
LUKESHIRU Author

That was kinda the point. I mentioned that the performance will be worse by the fact that we are wrapping our action on a promise, but in the example it takes less time because multiple promises can run at the same time, which is yet another plus. If we want to await, we don't do it for every single value, we can just use stuff like Promise.all, Promise.allSettled and so on. Putting just an await in front and calling it slower is almost like adding a timeout 😅

Collapse
qm3ster profile image
Mihail Malo

If you're so functional, just define your own Result type (or import a well thought out community one).
Don't abuse the native Promise that yields to the event loop. It's like putting every function call inside a closure in setimmediate.

Collapse
lukeshiru profile image
LUKESHIRU Author

The idea was to show that you can use native tools. You went from "promises aren't for that" to "but promises yield to the event loop". I get you don't want to use Promises to replace try/catch, but it has it valid usages (as I mentioned previously). You're thinking this as completely separated from other promises, but imagine it being used together with file interactions, or network interactions, and making your operations part of the then chain keeping the syntax consistent.

Collapse
jmr_code_social profile image
Jorge Marquez

Very very interesting article!!. Really liked the way you wrote this article, the way it gives alternatives to traditional thinking.

You just showed in a three article story the beauty of coding. You can have multiple ways to do things and none of them or all of them are the right way, it all depends on how you think and approach the situation.

Keep writing articles like this, it's a joy to read this kind of lectures.

Applauses for you!!
Kudos

Collapse
lukeshiru profile image
LUKESHIRU Author

Thank you a lot for the kind words, I have a few more articles for this series of post that I'll post in the near future. Hope y'all like them too ☺️

Collapse
ionellupu profile image
Ionel lupu

I am in the same boat: replace as many if statements as possible and I must say the code looks a LOT more cleaner.

But, I found more ways to remove some if statements thanks to this article.

Thank you!

Collapse
lukeshiru profile image
LUKESHIRU Author

Happy to be useful! Thanks for reading! ☺️

Collapse
uahnbu profile image
uahnbu • Edited on

Actually you can do

const welcomeMessage = ({admin}) =>
  `Welcome, ${admin ? 'administrato' : 'use'}r!`;
Enter fullscreen mode Exit fullscreen mode

🤣

Collapse
lukeshiru profile image
LUKESHIRU Author • Edited on

You could, but you shouldn't. You have to consider that the string could come from a constant, or a translations file for i18n.

Collapse
uahnbu profile image
uahnbu

Nah nothing. I see incompletion and I do optimization that's all 😜

Collapse
sebbdk profile image
Sebastian Vargr

I don't know, I usually avoid the usage of else & ternaries.
The exception being really small ternaries, as bigger ones are always hard to read.

Instead I use if blocks with return statements, that way I end up with fewer indentations, more block based code, and as a result more readable code.

Generally speaking though, I care much more about method/argument naming & keeping methods small, it's a much more productive use of my time.

If logic is self contained, the only the wrapping/interface matters in my opinion. :)

Collapse
dominikbraun profile image
Dominik Braun

Congratulations, you over-engineered some really simple code to an unreadable mess just to avoid an if ffs.

Collapse
lukeshiru profile image
LUKESHIRU Author

Congratulations, you completely missed the point of the post just to write a sarcastic comment!

Collapse
guypes profile image
Guido Pes

Very good, only about the average example, I'll use reduce to calculate the total and do only one division to obtain the average.

Collapse
lukeshiru profile image
LUKESHIRU Author • Edited on

I usually do that myself as well, writing the article I though how I will do that in a single reduce. I wanted to show that reduce itself is kinda complex generally speaking, and generally we should just use it when is necessary for stuff as sums or averages. I also completely forgot to show how it looks like when you move stuff to functions with clear names, so you go from array.map(/* a bunch of code here inline */) to something like array.map(double) which is way more readable. I update the article with a new section that I forgot from my notes here (new section "Edit: Improving readability").

Thanks for reading!

Collapse
ravichoudharygithub profile image
Ravi Choudhary

Great learning.

Collapse
rayhan_nj profile image
Raihan Nismara

doesnt matter, I use both

Collapse
lukeshiru profile image
LUKESHIRU Author

I guess you didn't got the point of the post, then. Read the disclaimer at the bottom, it might help.

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
lukeshiru profile image
LUKESHIRU Author

Such as? I only hide comments when they go against the code of conduct, like being disrespectful, out of topic and so on.

Some comments have been hidden by the post's author - find out more