Maybe You Don’t Need That Utility Library After All: Part II

Cory
7 min readDec 11, 2018

Disclaimer: This is not a dig at either of the fantastic underscore or lodash libraries, nor any other utility library. It is not a call to stop using any of them. Nor is a pitch for some replacement library. Rather this is a suggestion to stop and think. Is using a utility library improving the end user experience on a given project, or are we just falling into habit from previous projects?

As discussed in part I, there may be many reason to reach for a utility library. I hope to persuade you, dear reader, that one on those reasons ought not to be out of mere habit.

We began using the lodash library as a proxy for the many utility libraries out there, and selected several Array utility functions from it that had a native counterpart. We looked at how to use those particular functions in lodash, then implemented them using the native, built-in counter-part.

This time, we are going to look at those utility functions that don’t have a native counterpart, but are nonetheless trivial to implement. Again, the intent here is not to persuade you from using your utility library of choice, but to show you that you don’t necessarily need to. Let’s begin.

Trivial Native Solutions

_.compact

_.compact filters an array to only truthy values.

_.compact([0,1,false,2,'',3])    // [1,2,3]

this is perhaps the simplest useful example of a filter.

[0,1,false,2,'',3].filter(item => !!item)  // [1,2,3]

Because the filter method coerces every return value into a boolean, we don’t even need the !! coercion. To make things just a hair simpler, we can accomplish this with another utility function in lodash, and which is also exceedingly trivial. The identity function, which is just a function that returns the argument that is passed to it. It doesn’t sound that useful, but it has some surprising applications, such as this one. We will cover identity again when we get to lodash’s util category

_.difference

_.difference filters a target array to only those values the do not exist in the source array.

_.difference([2, 1], [2, 3]) // => [1]

Here, we can combine a simple filter, with the array’s includes method which returns true if the target item is present in the array, and false otherwise.

[2, 1].filter(item => ![2, 3].includes(item))  // [1]

_.differenceBy

_.differenceBy is almost identical to _.difference except that instead doing the check against the item itself, it allows the consumer to give it a custom function to get at the heart of what is being compared.

_.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor) // => [1.2]

In this case, Math.floor is called on each item before it is compared. This has the effect of comparing only the whole number portion of each item, ignoring the fractional part.

const iteratee = Math.floor
[2.1, 1.2]
.filter(item => ![2.3, 3.4].map(iteratee).includes(iteratee(item)))
// [1.2]

This is just a little more complicated than the difference example. All we are doing differently is calling iteratee on every item in the target array before checking for its existence in the source array that has also been mapped through the iteratee function.

This probably fine for small arrays, but we are mapping the entire source array for every item in the target array. We can optimize this quite a bit by storing a copy of the mapped source array from the start.

const iteratee = Math.floor
const mappedSource = [2.3, 3.4].map(iteratee)
[2.1, 1.2]
.filter(item => !mappedSource.includes(iteratee(item))) // [1.2]

_.drop

_.drop slices off from the beginning of an array the number of items specified defaulting to 1.

_.drop([1,2,3,4,5])    // [2,3,4,5]
_.drop([1,2,3,4,5], 2) // [3,4,5]

[].slice can be used exactly the same as _.drop. Simply pass [].slice a single positive integer. If no arguments are passed to [].slice, nothing is dropped. It effectively performs a shallow copy.

[1,2,3,4,5].slice()  // [1,2,3,4,5]
[1,2,3,4,5].slice(2) // [3,4,5]

_.dropRight

Similar to _.drop only _.dropRight slices off from the end of an array.

_.dropRight([1,2,3,4,5]) // [1,2,3,4]
_.dropRight([1,2,3,4,5], 2) // [1,2,3]

[].slice has an interesting feature where if you pass it a negative integer as it’s first argument, it will move backaward from the end of the array. So we can use it very similarly to _.dropRight.

[1,2,3,4,5].slice(-1) // [1,2,3,4]
[1,2,3,4,5].slice(-2) // [1,2,3]

_.first/_.head

These two are aliases for each other, so I’m just going to reference head . _.head return the first item of an array

_.head([1,2,3,4,5]) // 1

Of course, arrays can be accessed by index, so we can implement _.head trivially.

[1,2,3,4,5][0] // 1

We could also use destructuring.

const [head] = [1,2,3,4,5];
// head = 1

This is great because it is not uncommon to want `tail` along with `head`. We can use collect up the rest of the array using the rest (`…`) operator for a very succinct pattern.

const [head, ...tail] = [1,2,3,4,5];
// head = 1
// tail = [2,3,4,5]

_.flatten

As the name suggests, flatten will bring elements from sub-arrays, up one level.

_.flatten([1, [2, 3, 4], 5]) // => [1, 2, 3, 4, 5]

We can combine one syntax feature with one array method and achieve exactly the same effect. We spread the array we want to flatten as arguments into an empty array’s concat method.

[].concat(...[1, [2, 3, 4], 5]) // [1, 2, 3, 4, 5]

_.fromPairs

This method returns an object composed from key-value pairs.

_.fromPairs([['a', 1], ['b', 2]]) // => { 'a': 1, 'b': 2 }

There is a proposal to add a static fromEntries method on the Object class to do this natively, but since it’s not a laguage feature yet, we will implement one. We are change data structures here — array to object — so this is a great opportunity to use reduce where we will build up our object as we iterate over our array.

[[a, 1], [b, 2]].reduce((obj, [key, val]) => ({
...obj,
[key]: val
}), {}) // { a: 1, b: 2 }

For each item we iterate over, we create a new object that consists of the previous object (...obj) with the first item in the array we are currently iterating on as the object key ([key]) and the second item as its value (val).

_.initial

_.initial takes a slice of the source array up to, but not including the last item.

_.initial([1,2,3,4,5]) // [1,2,3,4]

We’ve seen how to use [].slice with one parameter when we looked at _.head. This first parameter is actually the beginIndex for where to start the slice. [].slice also takes a second parameter. The second parameter is the endIndex. endIndex tells [].slice where to stop and it is exclusive, meaning the item at that index will not be included. As with the beginIndex parameter, a negative endIndex parameter will refer to the end of the source array and go in reverse.

[1,2,3,4,5].slice(0, -1) // [1,2,3,4]

_.last

_.last is the inverse of _.first. It will return the last item in the array.

_.last([1,2,3,4,5]) // 5

We can get the last item directly like so:

[1,2,3,4,5].slice(-1)[0];

_.nth

_.nth will return the nth item from an array. Negative indexes return the nth from the end.

_.nth([1,2,3,4,5], 2)  // 3
_.nth([1,2,3,4,5], -2) // 4

Following similar previous patterns we can use slice to accomplish this. The difference is that slice will return an array, which we can unwrap if we need to by referencing the 0 index.

[1,2,3,4,5].slice(2,3)   // [3][0] => 3
[1,2,3,4,5].slice(-3,-2) // [4][0] => 4

_.take

_.take creates an array from 0 to n of the source array.

_.take([1,2,3,4,5], 3) // [1,2,3]

This is trivially accomplished with [].slice

[1,2,3,4,5].slice(0, 3) // [1,2,3]

_.takeRight

_.takeRight creates an array from n to arr.length inclusive.

_.takeRight([1,2,3,4,5], 3) // [3,4,5]

This is even more trivially accomplished with slice.

[1,2,3,4,5].slice(-3) // [3,4,5]

_.union

_.union([1,2], [2,3], [3,4]) // [1,2,3,4]

A union can be thought of as a concat followed by a dedupe. We’ve already seen concat, and we can leverage Set, which enforces uniqueness, to dedupe for us.

[...new Set([].concat([1,2], [2,3], [3,4]))] // [1,2,3,4]

_.uniq

_.uniq dedupes an array keeping the first occurrence of an item.

_.uniq([1,2,3,4,1,2,3,5]) // [1,2,3,4,5]

Set is a data structure similar to arrays, but which enforce uniqueness. We can leverage this feature to automatically dedupe an array by coercing the array to a Set and back to an array.

[...new Set([1,2,3,4,1,2,3,5])] // [1,2,3,4,5]

_.without

_.without takes an array as its first argument, and any values you wish to exclude from that array as its remaining arguments. It returns an array similar to the first array only “without” any items matching the arguments passed in.

_.uniq([1,2,3,4,5], 1, 3, 5); // [2,4]

This is little more than a filter which can be used to accomplish the same task.

[1,2,3,4,5].filter(item => [1,3,5].includes(item))

_.zip

_.zip takes any number of arrays and recombines them as arrays of items at corresponding indexes.

_.zip(['a', 'b'], [1, 2, 3], [true, false]);// => [['a', 1, true], ['b', 2, false], [undefined, 3, undefined]]

We will need to take this in two steps. First we need to find the largest of the arrays we are zipping together. Then, we map over that array, and return for each item an array of items from all arrays at that corresponding index.

const zip = (arrs) => arrs
.reduce((longest, next) =>
next.length > longest.length ? next : longest, [])
.map((_, i) => arrs.map((arr) => arr[i]))
}
zip([['a', 'b'], [1, 2, 3], [true, false]])
// [['a', 1, true], ['b', 2, false], [undefined, 3, undefined]]

_.zipObject

_.zipObject takes two arrays, treating the values in the first array as a collection of keys and the values in the second array as a collection of values and creates an object marrying the keys with the values

_.zipObject(['a', 'b'], [1, 2]);// => { 'a': 1, 'b': 2 }

There are many ways to approach this, but since we are changing data structures we are going to reach for reduce to help us. We iterate over the first array and build up an object with reduce using the index of the iteration to access the corresponding value in the second array.

zipObject = (arr1, arr2) => arr1.reduce((obj, key, i) => ({ …obj, [key]: arr2[i] }), {})

--

--

Cory

Front End Engineer with deep technical expertise in JS, CSS, React, Frontend Architecture, and lots more. I lean functional, but I focus on maintainability.