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 Programming , 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 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 , } ] 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 ¶

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 , } , { name : 'Ecuador' , topProducer : false , } ] , } const isSweet = fruit . sweet === true 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 )

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 )

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 , } , { name : 'Ecuador' , topProducer : false , } ] , } const isSweet = fruit . sweet === true 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 ) { 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 } if ( isBanana && isEcuadorian ) { alert ( 'i am an ecuadorian banana do not mess with me 🍌' ) return } 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' )

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:

inputs a key ( fruit ), and 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' )

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' )

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:

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 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 ) 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 ] ) 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!!!11

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.