Skip Main Navigation
Ben IlegboduBen Ilegbodu

9 single-statement JS algorithms for common data transformations

Reducing our reliance on Lodash by leveraging ESNext APIs to transform objects and arrays

Sunday, April 04, 2021 · 5 min read

Last month I wrote a post on a shorthand for converting a JavaScript array into an object lookup. I was able to write the code in a single statement:

const teamLookup = Object.fromEntries(teams.map((team) => [team.id, team]))

This exercise motivated me to investigate more single-statement data transformations we can use to limit our reliance on Lodash (and .reduce()). And now I'm posting about it. 😄

So here are the "rules":

  1. No using Lodash (kinda the whole point)
  2. No .reduce(). Pretty much any array or object can be transformed to another array or object within a .reduce() so that's no fun 😅
  3. Skipping the "obvious" new ES.next functionality (like .find(), object spread, etc)
  4. No data mutations; the transformation has to generate a new object (this is necessary for React and reducers)

Now that we've gotten that out of the way let's look at our transformations.


1. Delete an item from an array

JavaScript provides .splice() for removing elements from an array, but it happens in place. It mutates the array, which breaks one of our rules. But we can use .filter() instead!

const newPlayers = players.filter((player) => player.id !== ID_TO_DELETE)

By using .filter() to select all the players that do not have the ID_TO_DELETE, we delete the player with that id while simultaneously constructing a new array. This is probably one of my most favorite JavaScript "algorithms." I use it all the time.


2. Replace an item at an array index

To replace an item at an index in an array, we would normally assign the value.

players[indexToReplace] = newPlayer

But that, of course, mutates the array. And so does .splice(). Instead we can use array spread with .slice().

const newPlayers = [
  ...players.slice(0, INDEX_TO_REPLACE),
  newPlayer,
  ...players.slice(INDEX_TO_REPLACE + 1),
]

We take slices of the array before and after the target INDEX_TO_REPLACE. Using array spread we concatenate these slices around the newPlayer we want to add. This results in a new array with just our target index replaced. This does create 2 intermediary array slices. So if your array is sufficiently large this could be a performance hit.


3. Create an empty array of X items

The _.times() Lodash utility function is super helpful for quickly initializing an array of a specific length with values.

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

_.times(5, () => '')
// => ['', '', '', '', '']

_.times(5, (index) => index * 2)
// => [0, 2, 4, 6, 8]

Since using Lodash of course breaks one of our rules, we can use Array.from to accomplish the same thing.

Array.from({ length: 5 }, (_, index) => index)
// => [0, 1, 2, 3, 4]

Array.from({ length: 5 }, () => '')
// => ['', '', '', '', '']

Array.from({ length: 5 }, (_, index) => index * 2)
// => [0, 2, 4, 6, 8]

Array.from({ length: 5 })
// => [undefined, undefined, undefined, undefined, undefined]

This looks like we're breaking another one of our rules because we're just using Array.from (added in ES2015). But I'm allowing it because of the { length: 5} trick. Array.from accepts any array-like object, which is an object with a length property and indexed elements. Our {length: 5} object satisfies both requirements.

The second argument to Array.from is a mapFn. Since this {length: 5} trick creates an array with undefined values, the first argument in mapFn (the value) is useless. We only want the second argument (the index). We use the underscore (_) as a placeholder name for the function argument we do not need.


4. Map values of an object

5. Map keys of an object

The _.mapValues helper is another useful function from Lodash. It creates a new object with the same keys as the original, but with new mapped values. By combining Object.entries(), .map(), and Object.fromEntries(), we can build this ourselves.

const TEAMS = {
  ATL: {
    name: 'Atlanta Hawks',
    // more team data...
  },
  BOS: {
    name: 'Boston Celtics',
    // more team data...
  },
  // remaining 28 teams...
}

// add the ID to each team info
const transformedTeams = Object.fromEntries(
  Object.entries(TEAMS).map(([teamId, teamInfo]) => [
    teamId,
    { ...teamInfo, id: teamId },
  ]),
)
// {
//   ATL: { id: 'ATL', name: 'Atlanta Hawks ' },
//   BOS: { id: 'BOS', name: 'Boston Celtics' },
//   ...
// }

Let's break it down real quickly. First, Object.entries converts the object to an array of [teamId, teamInfo] pairs (called entries). Then we map over the array of pairs to return a new array of pairs. By the way, we also use array destructuring to create the teamId and teamInfo function arguments. The teamId remains the first item in the pair, but now the second item is the teamInfo with the teamId merged in. Lastly, Object.fromEntries transforms our array of [key, value] pairs into our resultant object.

This same algorithm can be used to replicate _.mapKeys as well. Instead of changing the 2nd item in the [key, value] pair, we change the first (the key). In fact, there's nothing stopping us from changing both items in the [key, value] pair!

Again, there are a number of intermediary arrays that get created in this transformation. But if your object isn't huge, it should be fine.


6. Create an array of unique values

7. Create a union of arrays

We can create a duplication-free version of an array using the combination of the Set object and array spread. No more need for _.uniq!

const AGES = [24, 64, 24, 42, 23, 55, 12, 42, 37, 35]

const uniqueAges = [...new Set(AGES)]
// => [24, 64, 42, 23, 55, 12, 37, 35]

Converting the AGES array into a Set object strips out all duplicates because a Set can only store unique values. We then convert the Set back into an array by spreading it (using ...) into an array literal because it is a built-in iterable.

A similar algorithm can be used to recreate _.union which creates an array of unique values from multiple arrays.

const GROUP_A = [24, 64, 24, 42, 23, 55, 12, 42, 37, 35]
const GROUP_B = [24, 55, 62, 23, 57, 472, 5, 48]

const allUniqueAges = [...new Set([...GROUP_A, ...GROUP_B])]
// => [24, 64, 42, 23, 55, 12, 37, 35, 62, 57, 472, 5, 48]

The difference is that we spread GROUP_A & GROUP_B together to create a single, concatenated array before constructing a Set object from it.


8. Pick from an object

9. Omit from an object

The _.pick and _.omit Lodash helpers frequently come in handy. They are opposites of each other. The _.pick function creates an object with just the specified keys picked from the original. While the _.omit function creates an object omitting the specified keys. We basically use the same algorithm as mapping values of an object, except we use .filter() (plus .includes()) instead of .map().

const TEAMS = {
  ATL: 'Atlanta Hawks',
  BOS: 'Boston Celtics',
  BKN: 'Brooklyn Nets',
  CHI: 'Chicago Bulls',
  CLE: 'Cleveland Cavaliers',
  CHA: 'Charlotte Hornets',
  // ...other 24 teams
}
const IDS = ['ATL', 'CHI', 'CHA']

const PICKED_TEAMS = Object.fromEntries(
  Object.entries(TEAMS).filter(([teamId]) => IDS.includes(teamId)),
)
// {
// ATL: 'Atlanta Hawks',
// CHI: 'Chicago Bulls',
// CHA: 'Charlotte Hornets',
// }

const REMAINING_TEAMS = Object.fromEntries(
  Object.entries(TEAMS).filter(([teamId]) => !IDS.includes(teamId)),
)
// {
// BOS: 'Boston Celtics',
// BKN: 'Brooklyn Nets',
// CLE: 'Cleveland Cavaliers',
// ...other 24 teams
// }

The difference between pick vs omit is the negation (!) of IDS.includes(teamId) within .filter(). By filtering down the entries to only the teams listed (or not listed) in IDS, the object created by Object.fromEntries has (or excludes) the selected teams.


What do you think of these? I'm sure there are many more single-statement data transformers, but these are the ones I use frequently and find the most helpful. I would love to see what other ones you use, so feel free to reach out to me on Twitter at @benmvp.

Keep learning my friends. 🤓

Subscribe to the Newsletter

Get notified about new blog posts, minishops & other goodies


Hi, I'm Ben Ilegbodu. 👋🏾

I'm a Christian, husband, and father of 3, with 15+ years of professional experience developing user interfaces for the Web. I'm a Google Developer Expert Frontend Architect at Stitch Fix, and frontend development teacher. I love helping developers level up their frontend skills.

Discuss on Twitter // Edit on GitHub