Imperative programming focuses on the commands for your computer to run. Declarative focuses on what you want from your computer. While an imperative approach can often be more performant by being closer to the metal, unless you are dealing with large datasets the advantage is likely negligible.
By manipulating and digesting your arrays in a declarative fashion, you can produce much more readable code.
Here are a few ways to do that.
Declarative array methods
Perhaps the most powerful array method is
.reduce. It works by calling a provided function against each item of the array. This callback accepts up to four arguments (although I find myself usually only using the first two):
previousValue, which is often referred to as the ‘accumulator’. This is the value returned the last time the callback was called
currentValue, which is the current item in the array
currentIndex, which is the index of the current item in the array
array, which is the full array being traversed
In addition to this callback, the method accepts an optional initial value as the argument. If an initial value is not provided, the first value in the array will be used.
A very simple example is a reducer for getting the sum of a collection of numbers.
The callback adds the
currentValue to the
accumulator. Since no initial value is provided, it begins with the first value in the array.
.map will similarly accept a callback to be called against each element in an array.
This callback accepts three arguments:
currentIndex, and the
Rather than keeping track of an accumulator, the map method returns an array of equal length to the original. The callback function “maps” the value of the original array into the new array.
An example of a simple map callback is one that returns the square of each number.
.filter accepts a callback with the same arguments as
.map. Rather than ‘transforming’ each value in the array like a
.map, the filter callback should return a ‘truthy’ or ‘falsey’ value. If the callback returns a truthy value, then that element will appear in the new array.
An example might be checking to see if a list of numbers is divisible by 3.
Tips for readable callbacks
1. Name your callbacks
This is perhaps the single biggest increase in readability for your array methods. By naming your array method callbacks, you get an instant increase in readability.
Compare these two:
By giving your callback a name, you can immediately get a better understanding of what the code is trying to accomplish. When naming, there are a couple things to keep in mind.
Be consistent. Have a good naming convention. I like to name all of my
.map callbacks as
toWhatever. If I am reducing an array of numbers to a sum,
If I am mapping an array of user objects to names,
toFullName. When using
.filter, I like to name my callbacks as
isNotWhatever. If I am filtering down to only items that are perfect squares,
Be concise. Your callback should theoretically only be doing one job — try and capture that job with a descriptive yet brief name.
2. Name your callback arguments
currentValue are easy to reach for when authoring code — they are so generic that they are never wrong. Because they are so generic, however, they don’t help the reader of the code.
Extending this even further — if you are manipulating an array of objects and are only using a few values, it might be more readable to use object destructuring in the parameter list.
3. Choose the right method
Earlier I mentioned that
.reduce was perhaps the most powerful array method. That’s because, due to its concept of an accumulator, it is infinitely flexible in what it can return. A
.map must return an array of equal length to the original. A
.filter must return a subset of its original. With
.reduce you can do everything that
.filter does and more… so why not always use
You should use
.filter because of their limitation. A reader of your code will know when they see a
.filter that it will be returning a subset, but if they see a
.reduce they may need to look over the callback before knowing this. Use the most specific method for the job.
4. Chain together small functions
Most of the examples so far have been fairly contrived to show how each of these works. Here is an example that more closely resembles a real life scenario: taking an array of objects, similar to what you might receive from an API, and formatting them for consumption on your app.
In this case, let’s say that we are receiving a selection of nearby restaurants from an API.
We want to digest (pun intended) this data by creating a list on our website of all nearby restaurants that are both currently open and serve food.
One method of achieving this is through a single large reducer.
However, this reducer is doing three things: checking if open, checking if its a valid establishment (not coffee), and mapping to the name.
Here is the same functionality written with single-purpose callbacks.
There are some other advantages to splitting up your functionality into multiple callbacks. If the logic to any of your filters changes, you can easily isolate exactly where this change needs to occur. You can also reuse the functionality of certain callbacks elsewhere (for example, you can filter to
This method also makes for easier testing — you can write unit tests for all of your building blocks, and when adding new functionality and you simply reuse these blocks and not need to worry about anything breaking.
Imperative and declarative both have their place. If you are going through large amounts of data and every millisecond counts, stick with
for loops. That is what is happening behind the scenes anyways.
I would argue in most cases, code readability (and therefore maintainability) is worth the tradeoff. By being intentional with how you use these callbacks, you can maximize that benefit.