Using filter, map, reduce and forEach

tech code typescript

I have a simple rule of thumb when choosing to use array methods, and I'd like to share it with you. The array methods I'm discussing here are:

  • arr.filter(y)
  • arr.map(y)
  • arr.reduce(y)
  • arr.forEach(y)

For me, each of these are answers to the question How does an array need to change?

Briefly, the rule of thumb is this: Use filter if the array length needs to get smaller; use map if the array length stays the same but the elements of the array need to change in some way; use reduce if the array length needs to change, especially bigger, or the array needs to be transformed into some other type, like into an object or a string; and use forEach only when side-effects are necessary

I use these methods all the time in my code, roughly in that order of priority.

If you're already familiar with these, that's it. That's the blog post. You can stop reading. All the rest is just expansion, reiteration. I should have just stopped there, but what kind of blog post would that be? Absurd, that's what

Or go ahead and skim the examples.

If you're not already familiar with these, this post is probably not the best introduction, but I'll give it a good go. MDN is a great resource to learn more about them.

filter #

Use filter if the array length needs to get smaller

filter is straight-forward: a filter removes unwanted stuff, and an array filter removes unwanted elements from your array. This leads inevitably to the array being the same length or smaller

filter takes a predicate for its argument, which is a function that answers a true or false question about that input. If the predicate returns true, the element is included; if it returns false, the element is excluded from the output array

filter examples #

Consider the array [1, "tooth", 3, "fort", "fie!", 6]

This is an array of mixed type, containing elements of type number and string. Let's apply a filter to remove strings from the array, so that it's an array only of numbers

Here is a predicate that returns true if the input is a number and false otherwise: (x) => typeof x === "number"

Putting them together, with filter:

[1, "two", 3, "four", "five", 6].filter((elem) => typeof elem === "number")

This line takes the array [1, "two", 3, "four", "five", 6], applies the filter (elem) => typeof elem === "number" to each element in the array and returns the result, [1,3,6], all and only numbers

Note that the new array is length 3, while the input array is 6. It was a good decision to apply filter in this case!

Here's an example from working code:

const searchSpace = dict.filter((entry) => entry.length == wordLength);

Here, dict is literally a list of words (e.g. "apple", "banana", "pear", ...) only, humongous and the predicate, (entry) => entry.length == wordLength, returns true if the number of characters in entry is exactly wordLength and false if it's any other length. Applying this predicate to dict returns an array of words that all have a length matching wordLength. The statement assigns all of this to the constant searchSpace

Marvelous! The new array searchSpace is smaller than the input array dict, consisting entirely of words that are only of length wordLength. Another win for the Rule of Thumb!

map #

Use map if the array length stays the same but the elements of the array need to change in some way

The map method transforms each element of an array into something else, and returns these new elements in a new array of exactly the same length. It takes as its argument a function that accepts an input value and returns another transformed value based on the input. map passes each element in turn to this function, and returns an array with that function applied to each value

map examples #

Consider the array [1, "tooth", 3, "fort", "fie!", 6] and the function (x) => typeof x === "number"? x : 0

As before, the array is of mixed type, consisting of number and string elements. The function checks if x is of type number and returns x unchanged if it is, or 0 if it's of any other type

Putting them togther with map:

[1, "tooth", 3, "fort", "fie!", 6].map((x) => typeof x === "number"? x : 0) which returns [1, 0, 3, 0, 0, 6]

Note that the input array length is 6, and the output array is the same length! I am pleased

From working code:

const objToQuery = (obj: {}) =>
Object.entries(obj)
.map((entry) => `${entry[0]}=${entry[1]}`)
.join("&")

(This is a TypeScript function, but just convert this part (obj: {}) to (obj) and it's JavaScript)

This objToQuery function accepts a simple object (e.g. {page:1,query:"lime"}) and returns a string (e.g. page=1&query=lime) representation of the object. This is useful to convert a (simple!) object into a URL query string

Briefly reviewing Object.entries (and cribbing from MDN), it takes an object and returns an array of its [key, value] pairs. So, using our example object, Object.entries({page:1,query:"lime"}) returns [["page",1],["query","lime"]]

Applying the line .map(entry => `${entry[0]}=${entry[1]}`) to [["page",1],["query","lime"]] returns a simple array ["page=1", "query=lime"]. Finally, applying .join("&") returns a string, each element of the array "joined" with &: "page=1&query=lime"

Note that the input array for map, [["page",1],["query","lime"]] is of length 2 (2 key-value pairs) and the output array ["page=1", "query=lime"] is also length 2 (2 strings). The input and output array stayed the same size, while each element was transformed! This is exactly as we predicted!! Astounding

reduce #

Use reduce if the array length needs to change, especially bigger, or the array needs to be transformed into some other type, like into an object or a string

reduce is misunderstood and mistrusted, but it's probably my favorite of the array methods (Here are two devs shamefully making fun of people who use it, but do use it whenever necessary, and use it confidently and with pride!). I think people are confused by the reduce name, which implies that the array is going to get smaller. Not so! Remember, the array size is going to change or become not an array. Any time you need to expand an array into a bigger array or convert it into something else, consider using reduce

reduce description #

Like the other array methods, reduce takes a function as an argument. Maybe confusingly, this function is called the reducer, but the name isn't important. It's just a function like any other. reduce, like the other methods, calls this function once successively for each element of its input array, in order. Unlike the other methods, this reducer function itself requires 2 arguments instead of 1. Also unlike the other methods, it is the second argument of this function that receives the element value, not the first

The reducer's first argument value is the output of the previous call to the reducer, or if there is no previous element (because it's the first one) it's by default the first element of the array

reduce returns whatever its reducer returns when called with the last element of its input array

More info at MDN

Let's have a quick example or two:

reduce examples #

Consider [1, "two", 3, "four"] and the function (prev, elem) => elem

That function takes two arguments and returns only the second one. Calling that function with ("a","b") would just return "b". It's a useless function on its own, but it's useful as an example. [1, "two", 3, "four"].reduce((prev, elem) => elem) will call that function successively, and the reducer will receive values and return them successively, like so:

// the first element of the array is 1
reduce( (1,1) => 1 )
// So, in this iteration, the first argument is
// by default the first element of the array (1),
// The second argument is the element itself,
// and this reducer returns the second argument, the element itself: value 1

reduce( (1,"two") => "two" ) // the first argument is the result of the previous iteration (1),
// but this reducer ignores it, and just returns the second argument
// the second argument is the value of the element of this iteration (value: "two"),
// and so this iteration returns "two",
// which becomes the first argument value for the next iteration:

reduce( ("two",3) => 3 ) // the first argument is the result of the previous iteration ("two"),
// the second argument is the value of the element for this iteration (3),
// and so it returns 3

reduce( (3,"four") => "four" )
// finally, the first argument is the result of the previous iteration (3),
// the second argument is the value of the element for this iteration ("four"),
// and so it returns "four", and since "four" is the last element of the array
// reduce itself returns "four as its final result

So [1, "two", 3, "four"].reduce((prev, elem) => elem) returns "four", which is the value of the last call to the reducer, which just returns the last element of the array

Note that the size of the input array is 4, and the output isn't an array at all, but a string!

array into one thing #

Consider the array [1, "two", 3, "four", "five", 6] and the function (str, elem) => `${str}&${elem}

We've seen the array before, an array of mixed types. The function just takes two arguments and joins them using &. Calling this function with "apple" and "banana" will return "apple&banana". That seems rather limited

What happens when we add it to reduce, as in [1, "two", 3, "four", "five", 6].reduce((str, elem) => `${str}&${elem}`) is a bit more interesting, because it returns "1&two&3&four&five&6"

Congratulations! We reinvented .join. No wonder those devs mocked reduce!

array changes size #

Let's strip duplicated elements out of an array!

Consider the function (arr,e) => arr.indexOf(e) >= 0? arr : [...arr, e]

This function takes an array as its first argument, arr, and any value at all as its second argument e. It checks arr.indexOf(e) to see if e is in arr. If it is (i.e. arr.indexOf(e) >= 0) then the function returns arr itself. But if the value e is not in arr (i.e. arr.indexOf(e) === -1) then it returns the array arr with e appended as its final value (i.e. [...arr, e])

So, this function would accept, say ([1,2], 2) and return [1,2]; and would accept ([1,2], 3) and return [1,2,3]. This function when used as a reducer in reduce will return another array with all duplicates stripped

Wait, I forgot to mention one thing before! reduce can take 2 arguments! The first is mandatory, and that's the reducer we've been talking about. The second, optional argument is the first value to be sent to the reducer. If it's not supplied, it's by default the first value of the array, as we discussed before. You okay? Let's take a look:

[1,1,1,2,2,3,3,4,4,4,5,6,6].reduce((arr,e) => arr.indexOf(e) >= 0? arr : [...arr, e], []) is going to return [1,2,3,4,5,6], which is the original array stripped of duplicates

Let's step through it

.reducer([],1) // is the first iteration.
// its first argument is `[]`, which we gave as the second argument to reduce, above
// its second argument, 1, is the element value of this iteration
// Reducer checks [] to see if it has `1` in it, and it doesn't, so reducer returns `[1]`

// ...which now becomes the value of the first argument sent to the second iteration:

.reducer([1],1)
// reducer checks `[1]` to see if it contains `1`
// It does! So reducer returns `[1]`

// the next iteration is identical, because arr[3] is value 1
// So skipping ahead to:

.reducer([1],2)
// reducer checks `[1]` to see if it contains `2`
// it does *not*, so reducer returns `[1,2]`
// ... which is the first argument of the next iteration...

// and so on, until the last element of the array:
.reducer([1,2,3,4,5,6],6)
// reducer checks the array to see if 6 is in it, and it is
// so it returns the array itself, unaltered
// and since `reduce` is at the end of the input array
// `reduce` returns the first argument value,
// the array is the solution!

Stick all of that in a named function, and you can strip duplicates out of any array you pass in

const uniq = (arr) => arr.reduce((acc,e) => acc.indexOf(e) >= 0? acc : [...acc, e], [])

So

uniq([1, 1, 1, 2, 2, 3, 3, 4, 4, 4, 5, 6, 6]) //  => [1,2,3,4,5,6]

What else can reduce do? #

Oh, lots. Maybe you have a succession of functions and you want to pass the result of one into the next one in turn. For instance, if you want to extract the user id and password sent in an auth header, you could do something like this:

export function getUserIdPassword(headers) {
const headerValue = getAuthHeaderValue(headers)
const decodedHeader = decodeAuthHeader(headerValue)
const parsedHeader = parseAuthHeader(decodedHeader)
return parsedHeader
}

Or you could do:

export const getUserIdPassword = (headers) =>
[getAuthHeaderValue, decodeAuthHeader, parseAuthHeader].reduce(
(acc, func) => func(acc),
headers
)

Personal preference. No judgment.

Maybe you want to create an object out of key-value pairs. You can do:

const toObj = (keyValues) =>
keyValues.reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {})
toObj([
["page", 1],
["fields", "size,weight,color"],
["priority", "A"],
]) // => {page: 1, fields: "size,weight,color", priority: "A"}

In all of these cases, we're essentially converting an array into something else, maybe another different array. If you're doing that, reduce might get you there faster.

forEach #

use forEach only when side-effects are necessary

Briefly, a side effect is when a function changes a value or has an effect outside of its own scope. So, a quick f'rinstance:

let count = 0
function incr() {
count = count + 1
}

Calling incr() causes a side-effect of incrementing count by 1

So, if you need to do something like that, forEach is your method. The function that forEach accepts as an argument returns no value. Nothing. It's of type void. So:

(elem) => {} is pretty much it

Maybe you want to clear a signup form:

const clearSignupForm = () =>
Array.from(getElem("form").querySelectorAll("input")).forEach(
(i) => (i.value = "")
)

Or maybe you want to log array contents to your console:

data.forEach((elem, i) => console.log(i, elem))

Personally, I use forEach only when a side effect is the only solution possible. I never do something like:

let keyValObj = {}

[["page",1],["fields","size,weight,color"],["priority","A"]].forEach([key,value] => keyValObj[key] = value)

It becomes difficult to know what's going on with keyValObj as the codebase grows. Better to have the direct assignment:

const keyValObj = [
["page", 1],
["fields", "size,weight,color"],
["priority", "A"],
].reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {})

Extraneous addendium #

I didn't tell you something. All of these array methods take functions as arguments... MDN calls them callbacks so let's do that. All of these array methods take callbacks as we've discussed. The thing I didn't mention is that these array methods pass more values to the callbacks than just the current value of the array. They also receive the current index of the array, and after that, the entire array. That looks like this:

Lets pass into ["zero", "one", "three"].forEach this callback: (elem, index, arr) => console.log({ elem, index, arr }) and see the output.

;["zero", "one", "three"].forEach((elem, index, arr) =>
console.log({ elem, index, arr })
)
// {elem: "zero", index: 0, arr: ["zero", "one", "three"]}
// {elem: "one", index: 1, arr: ["zero", "one", "three"]}
// {elem: "three", index: 2, arr: ["zero", "one", "three"]}

As we see here, the elem value is followed by the index value of the current array, followed by arr which contains the entire input array

Same goes for the callbacks on all the other methods, including reduce. In case you find it useful

Also, you can chain these methods together. Here's an example of how that's done:

exampleEnvEntries = exampleEnv
.replace(/\r/g, "\n")
.replace(/\n{2}/g, "/n")
.split("\n")
.map((l) => l.trim())
.filter((l) => !l.startsWith("#")) // eliminate comments
.filter((l) => l.length > 0) // eliminate blank lines

This takes a string called exampleEnv, splits it into an array which holds one line of text per element, removes blank spaces at the beginning and end of each of those lines, filters out any that start with # and discards any blank lines

Later, this example file is tested against the user's file and warns the user if their file is missing something or if they have not changed something they should have changed, using forEach:

exampleEnvEntries.forEach((line) => {
const [varName, varValue] = line.split("=")
test(`${varName} is defined as an environmental variable`, () => {
expect(process.env[varName]).toBeDefined()
})

// The value of each SECRET or PASSWORD in 'example.env' is not the same as in process.env
if (varName.indexOf("SECRET") >= 0 || varName.indexOf("PASSWORD") >= 0)
test(`${varName} is not ${varValue}`, () => {
expect(process.env[varName]).not.toBe(varValue)
})
})

Comments:

Leave a comment

There was an error. It's me, not you. You could try again later? Or try another method? Email? Smoke signals? Tell me what went wrong!

please enter a substantive message with no link or website address
please enter your name
please enter an email

Comment successfully sent, and it awaits moderation.