diessi.caBlog
October 30, 2017

Expressive JavaScript Conditionals (for Humans)

Conditionals! As programmers, we write at least one every day. Easy to write, horrible to read, and sometimes it seems as though there’s no way to work around that.

Today, I’m happy to share some patterns I’ve learned over time and have spread heavily in code reviews. Let’s rethink conditionals to make the most out of a language’s power of expression – in the case of this article, JavaScript.

Patterns for Conditionals

First, to put you in the mood, a quote from Literate Programming1, by Donald Knuth.

The practitioner of literate programming can be regarded as an essayist, whose main concern is with exposition and excellence of style. Such an author, with thesaurus in hand, chooses the names of variables carefully and explains what each variable means. He or she strives for a program that is comprehensible because its concepts have been introduced in an order that is best for human understanding, using a mixture of formal and informal methods that reinforce each other.

That sums up the approach for this post: purely aimed at how to write code for people to read. Performance doesn’t matter now, since 1) that’s not the goal and 2) I don’t believe in premature optimisation.

I’ll briefly talk about each pattern, but what’s more important here is to read the code carefully and get something out of it (as if it were poetry or something!).

1. Variables that hold conditions

When your condition is made up of an expression (or many of them), you can store it within meaningful-named variables.

Let’s say, to illustrate, that our program holds some data about a fruit and we have to check whether that fruit is a banana. We could do this:

const fruit = {
    colors: ["yellow", "green"],
    sweet: true,
}

if (fruit.sweet === true && fruit.colors.some((color) => color === "yellow")) {
    alert("do your stuff 🍌")
    return "is banana"
}

Or we could store our conditions in variables!

const fruit = {
    colors: ["yellow", "green"],
    sweet: true,
}

const isSweet = fruit.sweet === true // or just `fruit.sweet`
const isYellow = fruit.colors.some((color) => color === "yellow")
const isBanana = isYellow && isSweet

if (isBanana) {
    alert("do your stuff 🍌")
    return "is banana"
}

Not all conditions need to be in a variable. fruit.sweet, for instance, is quite expressive by its own.

This way, if we or our workmates need to know what makes a fruit a banana in our program, it’s just a matter of checking out the variable itself. The logic for how conditions evaluate will be there, behind a meaningful name.

It’s even more useful for composing and reusing conditions.

Filtering arrays

In the previous example, all variables were conditions based on the fruit object, for didactic purposes. Commonly, though, those variables actually store (pure) functions that take any fruit and test against it. I'm changing that to fit this example.

Let’s say we’ve got an array of fruits and we need to filter all the bananas out of it.

const fruits = [
    {
        colors: ["yellow", "green"],
        sweet: true,
    },
    {
        colors: ["red"],
        sweet: false, // don't know what kind of fruit this'd be
    },
]

const isSweet = (fruit) => fruit.sweet === true

const isYellow = (fruit) => fruit.colors.some((color) => color === "yellow")

const isBanana = (fruit) => isSweet(fruit) && isYellow(fruit)

const bananas = fruits.filter(isBanana)

So expressive! I love the way that I can read that last line and even a kid (a non-programmer one!) can understand. (Yes, I’ve tried that.)

The currying technique would help introduce expressiveness without the verbosity for isBanana. An optional appendix at the end of the article elaborates more on that, if you’re curious.

That’s it. Hold on to that one though – it’s the foundation for what’s next.

2. Define value conditionally

Still considering the variables from the example above, this could also be done:

const myFruit = isYellow && isSweet && "banana"
return myFruit

The value for the myFruit variable will only be assigned to “banana” if those conditions are true. Pretty useful for when values are defined conditionally!

That saves you from if (isYellow && isSweet) [...], and it’s quite expressive, I’d say. An expressive expression. 👌

3. Define value conditionally with fallback

Conditionals, a comic from xkcd
xkcd: Conditionals

What if we want something else in case it’s not a banana? Instead of an if (isBanana) [...] with else, go for the ternary operator.

const isBanana = isYellow && isSweet
const myFruit = isBanana ? "is banana" : "is other stuff"
return myFruit

Some additional but important advice I’d give regarding ternary expressions:

  • Make them as compound as possible.
  • Do NOT nest them. Don’t pretend the nesting is readable – it’s not even human to do that.
  • Don’t avoid writing if statements because you want to look smart.

4. Check for all conditions

We’ve (finally) found out that checking for isYellow and isSweet isn’t enough to make sure the fruit is a banana. After all, yellow pepper is both sweet and yellow and it’s still not a banana, right?

Right. We then add more checks to the isBanana variable, and this time they’re ugly ones: they check whether the fruit is either from Brazil or Ecuador, both top banana producing countries.

const fruit = {
    colors: ["yellow", "green"],
    sweet: true,
    countries: [
        {
            name: "Brazil",
            topProducer: true, // i'm brazilian and therefore biased
        },
        {
            name: "Ecuador",
            topProducer: false,
        },
    ],
}

const isSweet = fruit.sweet === true // or just `fruit.sweet`
const isYellow = fruit.colors.some((color) => color === "yellow")
const isBrazilian = fruit.countries.find((i) => i.name === "Brazil")
const isEcuadorian = fruit.countries.find((i) => i.name === "Ecuador")
const isBanana = isYellow && isSweet && isBrazilian && isEcuadorian

if (isBanana) {
    alert("do your stuff!11 🍌")
    console.log("now this is really a banana")
}

Did you see how big isBanana is getting? We’d have to start breaking lines in those && to improve readability, which, personally, I don’t like doing.

If refactoring the booleans into new variables isn’t an option anymore, what about storing the conditions in an array and testing every item for truthiness?

Use the power of array’s every:

const isBanana = [isYellow, isSweet, isEcuadorian, isBrazilian].every(Boolean) // or `x => x === true`

Repetitive friendly reminder: don't store every condition in a variable. Remember you can always add the condition itself to the array.

You can even turn that into a snippet:

const all = (arr) => arr.every(Boolean)

const isBanana = all([isYellow, isSweet, isEcuadorian, isBrazilian])

I don’t know how useful that looks for you, but, in everyday work, I really appreciate doing it.

5. Check for any condition

What if we’ve got crazy and we’re fine considering something a banana when it’s either yellow, Brazilian or sweet?

Use the power of array’s some:

const isBanana = [isYellow, isSweet, isBrazilian].some(Boolean) // or `x => x === true`

Yeah, yeah, I know, you can just use the OR operator.

If any of them evaluates to true, profit! – it’s a banana. 🍌

const any = (arr) => arr.some(Boolean)

const isBanana = any([isYellow, isSweet, isBrazilian])

In other words, being a Brazilian means you’re a banana! It doesn’t sound any bad to me honestly.

Pretty similar to Ramda’s either. 😎

6. Early return

We want something special for when the banana is Brazilian! else conditions would do the trick, wouldn’t they?

const fruit = {
    colors: ["yellow", "green"],
    sweet: true,
    countries: [
        {
            name: "Brazil",
            topProducer: true, // i'm brazilian and therefore biased
        },
        {
            name: "Ecuador",
            topProducer: false,
        },
    ],
}

const isSweet = fruit.sweet === true // or just `fruit.sweet`
const isYellow = fruit.colors.some((color) => color === "yellow")
const isBrazilian = fruit.countries.find((i) => i.name === "Brazil")
const isEcuadorian = fruit.countries.find((i) => i.name === "Ecuador")
const isBanana = isYellow && isSweet

if (isBanana && isBrazilian) {
    // first true case!
    alert("i am a brazilian banana i love football 🍌")
} else if (isBanana && isEcuadorian) {
    alert("i am an ecuadorian banana do not mess with me 🍌")
} else {
    alert("a normal banana from somewhere else")
}

Alternatively, THOOOUGH, we can early return (nothing at all, i.e. undefined), which will make our code stops its flow at that point.

if (isBanana && isBrazilian) {
    alert("i am a brazilian banana i love football 🍌")
    return // sTAAAAHP!!!1
}

if (isBanana && isEcuadorian) {
    alert("i am an ecuadorian banana do not mess with me 🍌")
    return // DON'T GO ANY FURTHER, JAVASCRIPT!11
}

// or do, whatever
alert("a normal banana from somewhere else")

Those checks could also be refactored into a variable named isBrazilianBanana. I found it too much for this example though, so this is just a friendly reminder that your conditions can always be composable.

Keep in mind that early returns might make the flow of your program confusing. Different from when you’re using else conditions, it’s not explicit that the conditionals are from the same logical group anymore.

7. Check for the same variable

Let’s get the colour of the fruits! Each fruit has one.

Hmmm… Different cases that require different handling, but all handled cases depend on the same thing. What do we do? We conditionally check for its value and handle the case depending on that.

const getFruitColor = (fruit) => {
    if (fruit === "banana") {
        return "is yellow"
    } else if (fruit === "apple") {
        return "is red"
    } else if (fruit === "orange") {
        return "is orange"
    }
}

getFruitColor("banana") // => is yellow

This code would benefit from early returns, and, of course, the switch statement.

Or we don’t. Because, alternatively, we can create a getter function (getFruitColor) that:

  1. inputs a key (fruit), and
  2. outputs the value that was assigned to that key in a specific map (fruitColors).

Like this:

const fruitColors = {
    banana: 'is yellow'
    apple: 'is red',
    orange: 'is orange',
}

const getFruitColor = fruit => fruitColors[fruit]

getFruitColor('banana') // fruitColors['banana'] => is yellow

Simplifying, since that map isn’t useful outside of the getter itself anyway:

const getFruitColor = fruit => ({
    banana: 'is yellow'
    apple: 'is red',
    orange: 'is orange',
}[fruit]

getFruitColor('banana') // => is yellow

This is a very common technique I first saw on a blog post from Angus Croll in 2010. I love the simplicity of it.

Nowadays there’s even more freedom with this technique, considering features such as computed property names for objects. But don’t get too creative with that! Go for what’s more readable and expressive for you AND the people you work with.

Appendix: Currying, because verbosity isn’t expressiveness

This is an additional reading and requires familiarity with curried functions. For further reading on the currying technique, I recommend the "Currying" chapter of the book Mostly Adequate Guide to Functional Programming, or A Beginner’s Guide to Currying, as a more beginner-friendly introduction.

Let’s reconsider:

  1. the problem from the 1st, where we had an array of fruits and had to create a function that checks whether the fruit passed to it was a banana, and
  2. the all util from the 4th pattern.

Check for all conditions, revisited

Imagine if we had a lot of conditions for isBanana. We could use && or even all.

const isBanana = (fruit) =>
    isBanana(fruit) &&
    isSweet(fruit) &&
    isEcuadorian(fruit) &&
    isBrazilian(fruit)
// or
const isBanana = (fruit) =>
    all([
        isYellow(fruit),
        isSweet(fruit),
        isEcuadorian(fruit),
        isBrazilian(fruit),
    ])

It’s meaningful, easy to read, but introduces too much verbosity. The currying technique could help us introduce more meaning without being verbose. With some changes in the all util, that could be boiled down to:

const isBanana = all([isSweet, isYellow, isEcuadorian, isBrazilian])
// no explicit fruit here, and still works!
const bananas = fruits.filter(isBanana)

Notice we’re not explicitly passing fruit anymore. How do we do that? By making all a curried function. There are some ways to achieve that, here’s one:

const all = (conditions) => (currentItem) =>
    conditions.map((condition) => condition(currentItem)).every(Boolean)

It takes the array of conditions and returns a function that takes the current item and calls each condition with that (as argument!). At the end, we check whether they all evaluated to true. It’s not magic, it’s CURRYING!!!112

That’s, of course, a simplistic implementation, which has costed me an array (from map). What’s important to get is how we play with function arguments under the hood to achieve currying.

You can curry your functions using Ramda’s curry (or Lodash’s, whatever); or, if you’re interested on that as an util, Ramda’s all and its source are really worth-checking!

Final considerations

By striving for conditionals as expressions other than statements, you write code in a functional approach. Code with a clearer sense of what it’s being checked without having to unearth to its foundations. Code that’s easier and faster for people to reason about.

But if your code is fine with an if rather than an object lookup or a ternary operator, just stick to it.

It’d break the whole purpose of this post if those patterns were used for looking smart. However, if refactoring your conditionals with them would lead to a more expressive codebase, go for it! Play with the patterns, compound them. Use your creativity to communicate well.

It’s a language, after all.

1

A programming paradigm first introduced by Donald Knuth, with the goal of providing an alternative, human, perspective on the programmer’s motivation. Read the article.

2

It may seem more magic than currying sometimes. JavaScript isn’t a purely functional language, so such techniques aren’t as popular as they are in Haskell community, for instance. Simplicity conflicts with easiness here.