Today we’re going to have some fun with JavaScript Expressions, and explore some interesting syntaxes. JavaScript offers many ways of implementing logic, and you probably use them on a daily basis.
The most common way we’d typically write logic is using statements. For example, inside curly braces {}
, using something like an if
or else
statement.
Table of contents
In this article though, we’ll explore an alternative way of writing logic using the ‘magic’ of JavaScript expressions, combined with commas and other pieces. If you’ve ever seen or wondered what wrapping code in brackets ()
will do, this post is for you. Prepare for expressions!
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
Please note! This way of writing is not the most readable, it’s opinionated and more difficult to read. But it will teach you more how JavaScript works so you can become a better developer - think about minified code for example, it’s the result of your beautiful JavaScript transformed into the most minimal of expressions to cut down the size of the file. So this is what we’ll almost be doing, there’s definitely value in exploring the JavaScript language capabilities!
Grab yourself a fresh StackBlitz JS project and follow along!
Further reading after this article: I’d recommend Expressions versus Statements for a deep-dive into the subject of expressions and statements!
Traditional statement based logic
Let’s take a function that accepts value
and threshold
as arguments and returns an array of values.
An array contains number 5
as initial value and pushes passed in value as a second value.
If passed in threshold is less than 10
then, a constructed array is returned. Else, number 7
is pushed onto an array and that array is then returned.
There is several ways to write this code but it roughly would look like this:
const doSomethingUsingStatements = (value, threshold) => {
const arr = [5];
arr.push(value);
if (threshold < 10) {
return arr;
}
arr.push(7);
return arr;
}
Our code would then produce something like:
// Returns [5, 4, 7]
doSomethingUsingStatements(4, 10);
// Returns [5, 3]
doSomethingUsingStatements(3, 2);
Expression based implementation
Okay, get ready for expressions!
Our function signature will be the same but implementation details will vary. What we need first is an array containing number 5
.
To achieve that, we will use immediately invoked function to which we will pass our array:
const fn = () => (arr => console.log(arr))([5]).
This code is equivalent to:
const fn = () => { const arr = [5]; console.log(arr); }
When we run this, we will see [5]
in the console - but we will also notice undefined
is also present. This is because both versions of the code are not returning anything other than logging passed in an array.
Now, our statement based implementation does not return anything (no return
keyword) so by default it’ll return undefined
.
With our expression however, the last evaluated expression is returned (which in this case is the console.log
and returns nothing) which is why we see undefined
in the console.
For a function to return something, we need to either use the return
keyword (in our “statement based” function) or ensure that last listed expression will return a value (in our “expression based” implementation).
This basically means that we need to change the code to:
const fn = () => (arr => (console.log(arr), arr))([5]);
const fn = () => { const arr = [5]; console.log(arr); return arr; };
With the above implementations, we can notice that [5]
is written twice to the console, first due to using console.log
function and the second time due to [5]
being returned from the function call.
It’s also important to notice additional parentheses enclosing (console.log(arr), arr)
expression. This is due to the comma being used within the expression, and you can read more details about how this works in the subsection on commas in JavaScript!
Logic and Expressions
Looping back to our first code example, if we extract the “logic” inside the function, we’re left with:
{
const arr = [5];
arr.push(val);
if (threshold < 10) {
return arr;
}
arr.push(7);
return arr;
}
In the expression form, we can instead write this logic as follows:
(arr.push(val), threshold < 10 ? arr : (arr.push(7), arr))
The first thing we do here is pushing the passed in value onto the output array, which produces [5, val]
.
Next, using a ternary operator, we check if the threshold
parameter is below 10
. If it is, we just return arr
.
If the value is an above or equal threshold, we are pushing 7
onto return array and then return the array.
Putting the pieces together, our expression based implementation of our initial function is:
const doSomethingUsingExpressions = (val, threshold) => (
(arr) => (arr.push(val), threshold < 10 ? arr : (arr.push(7), arr))
)([5]);
Yes I know, it’s harder to read, but you’re learning new things about JavaScript and can even get a little fancy in your own projects if you feel like it.
Expressions: a step further
There is one elegant ‘improvement’ that we can make to an expression based implementation.
Instead of returning array on both cases of the ternary operator, we can return the output array at the end of the expression and utilize a ‘do nothing’ function.
A do nothing function is literally:
() => {}
This function is used commonly used in functional programming when using a ternary construct and is available in many JS libraries (e.g. noop in RxJS).
Re-implementation of our expression utilizing noop function looks like this:
(arr.push(val), threshold < 10 ? () => {} : arr.push(7), arr)
Which in full form, is:
const doSomethingUsingExpressionsWithNoop = (val, threshold) => (
(arr) => (arr.push(val), threshold < 10 ? () => {} : arr.push(7), arr)
)([5]);
This looks much more elegant (and follows separation of concerns) as it’s more clear where logic goes and where the return value is.
As a side note, a noop
function is so commonly used that it could be easily extracted into separate function and used throughout the code:
const noop = () => {}
Giving us something like this:
const doSomethingUsingExpressionsWithNoop = (val, threshold) => (
(arr) => (arr.push(val), threshold < 10 ? noop : arr.push(7), arr)
)([5]);
Note that noop
does not need to be function at all, some ways of implementing noop
are:
const noop = {};
const noop = undefined;
const noop = null;
All of the above will achieve the same result.
This could be refactored further into higher order function where we could remove a need for using ternary operator:
const whenTrue = (predicate, fn) => predicate() ? fn : undefined;
…but that’s material for a separate blog post!
Anyway, check out the source code of what we’ve covered so far:
One more example
To consolidate our knowledge further, here’s how this way of writing logic can be used inside the array’s built-in reduce
function.
A typical way of implementing logic inside a reduce
function is to use statements like that:
const statementInReduce = vals.reduce((a, c) => {
a.push(c);
return a;
}, [4, 5]);
The key element here is:
{
a.push(c);
return a;
}
This, with expression based implementation, can be rewritten simply to:
(
a.push(c),
a
)
Which, inside full expression based reduce will look like:
const expressionInReduce = vals.reduce((a, c) => (a.push(c), a), [4, 5]);
I admit that this doesn’t save as much as it only saves us from explicitly utilizing a semicolon and writing explicit returns - but in more complex examples can improve the elegance of the code somewhat.
Check out the working source code!
Conclusion
Expressions offer an interesting option to alter the look of the code. I find it most useful when writing code that’s more of a functional programming style. I hope that you found this to be an interesting exploration into JavaScript capabilities!