Human Readable JavaScript


For a long time, humans needed to "speak" like machines in order to communicate with them. And that's still true, we still have the need for people who work in assembly and other low-level languages. But for many of us, these complexities are abstracted away. Our job, is to focus on what is readable for humans and let the machines interpret our code.

This consideration is never more apparent than a situation in which identical code can be written in numerous ways. So today, I want to talk less about how something works, and more about how it reads. There is another post in here somewhere about functional JavaScript, but let's assume we're talking about map.

map is a function available for arrays in JavaScript. Think of it as for each. It takes a function as an argument and runs each element in the array through that function. The difference is that it doesn't alter the original array at all. The result is a new array.

Example

const arr = [1,2,3]
let multipliedByTwo = arr.map(el => el*2)
// multipledByTwo is [2,4,6]

Ok, so we know what map does. But look at the code snippet above. An incredibly terse function that multiplies a variable by two.

So let's take a look at all the different ways we could write that same logic.

Optional Parentheses

The first optional addition we can make is to add parentheses to the parameter definition of the internal function. This makes that piece of code start to look more like a typical function definition.

const arr = [1,2,3]
let multipliedByTwo = arr.map((el) => el*2)

What's interesting about this is that the only reason we don't need them is because we're only passing one argument.

const arr = [1,2,3]
let multipliedByTwo = arr.map((el, index) => el*2)

In cases where we pass more than one argument, the parens are not optional. Our example is map, if it were reduce we would always use the parentheses.

So let's take stock for a moment. Do we lose anything by adding the parentheses? Do we gain anything? We're adding two characters, what information does that convey? These are the things we need to ask ourselves as we develop code for our teammates and future selves to maintain and read.

Curly braces and return

We can go a step further with making that internal function adhere to official function syntax. Doing so requires curly braces and the return keyword.

const arr = [1,2,3]
let multipliedByTwo = arr.map((el) => { return el*2})

How do we feel about this code now? It certainly reads more clearly as a function. Do the braces and return add more bulk? Does our view of this change depending on the logic being returned?

As it turns out, this is again non-optional if our function logic is more than one line.

const arr = [1,2,3]
let multipliedByTwo = arr.map(
(el) => { if(el%2 === 0) { return el*2 } else { return el+1 }
})

Interesting. Does our opinion of the extra characters change based on the use case? What does that mean for consistency throughout our code?

Use a separate function

As we know and have seen, map takes a function as an argument and passes each element in our array into it. Perhaps we could, or should, define our internal logic outside of the map. As it stands, it looks a bit like pyramid code.

const arr = [1,2,3] const timesTwo = (el) => el*2 let multipliedByTwo = arr.map((el) => timesTwo(el))

What do we think? Realistically it's almost the same number of characters as the original version. But what about our example from above with more complex logic?

const arr = [1,2,3] const timesTwoOrPlusOne = (el) => { if(el%2 === 0) { return el*2 } else { return el+1 }
} let multipliedByTwo = arr.map((el) => timesTwoOrPlusOne(el))

Did this change your view? Or does it look cluttered and repetitive?

Just a function

Functional programming is an interesting paradigm. In part because of the way it allows us to write code. Again we're reminded that map takes a function as an argument. So why not give it a function.

const arr = [1,2,3] const timesTwo = (el) => el*2 let multipliedByTwo = arr.map(timesTwo)

Yes, this is valid. map knows to pass the element it gets to the function and use the result. We can get even more in the weeds by determining what form our timesTwo function could take. Right now it's a terse one-liner.

And note that map is really smart. We can pass the same function even if that function now uses both the element and the index to arrive at a return value!

const arr = [1,2,3] const timesTwoPlusIndex = (el, index) => (el*2) + index let multipliedByTwo = arr.map(timesTwoPlusIndex)

Does this seem readable? multipledByTwo is certainly pleasant to read, but where is timesTwoPlusIndex located in our codebase? Is it hard to track down? If someone is looking at this for the first time do they know it's a function? Or do they assume it's an object or array variable?

Functions are objects in JavaScript, but ignore that duplication for the moment.

How do we determine what is readable

There is no one size fits all syntax. Who is your audience? Polyglots or JavaScript experts? Who is maintaining your code? How many people work in this codebase? All of these things matter.

It entirely depends on the use case, and consistency is important. However, seeing all the different representations of the same functionality is eye-opening. All of these examples will be built into the same minified code. So the decision for us, as developers, is based on human readability. It's completely absent of machine performance and functionality considerations.

I've posed a lot of questions and not a lot of answers. I have my own opinions but would love to hear yours. Which of these are the most readable? Are there versions you prefer to write? Let's discuss it below!