Understanding the flatMap function

If you, like me, have spent any time in MDN's Array reference, you've probably run across flatMap. It's description is rather impenetrable:

It is identical to a map() followed by a flat() of depth 1

For quite some time, I reacted to that description with head scratching and general puzzlement. But relatively recently I began to understand what flatMap is good for. My goal, dear reader, is to impart this knowledge to you.

The Punchline

The normal map function lets us perform a 1-to-1 mapping. For each input element we get back exactly one output element.

What flatMap allows us to do is perform a 1-to-any mapping. For each input element we can choose to:

In my experience, I mostly find flatMap useful for its ability to return any number of output elements for each input element. The ability to filter elements at the same time is a nice bonus.

Let's move on to an example to show how flatMap might be useful in an almost-like-real-life sort of scenario.

Example

Imagine we are working on a website for a bookshop, and we have the following data structures:

// typescript
interface Author {
  name: string;
  books: Book[];
}

interface Book {
  title: string;
  published: Date;
}

Suppose we want to write a function that takes a list of Author objects and returns a list of all of the Books. What are some ways we could write such a function without flatMap?

// typescript
const getAuthorsBooksIter = (authors: Author) => {
  const books = [];
  for (const author of authors) {
    for (const book of author.books) {
      books.push(book);
    }
  }
  return books;
}

// Or a slightly more functional approach
const getAuthorsBooksFunc = (authors: Author) =>
   authors.reduce(
    (books, author) => books.concat(author.books)
  );

Of course, you can write this with flatMap too, and it's much shorter (otherwise it would've made for a pretty lousy example):

// typescript
const getAuthorsBooksFlatMap = (authors) =>
  authors.flatMap(author => author.books);

Another situation where flatMap is very helpful is when writing recursive functions. Say you want to write a function that takes in an object and produces a list of the primitive values from that object.

// input
{
  foo: 'bar',
  baz: {
    qux: 100,
    zap: {
      zorp: false
    }
  }
}

// output
['bar', 100, false]

We can write this function relatively easily with flatMap:

const getObjValues = (o) => {
  if (typeof o === 'object' && o !== null) {
    return Object.values(o).flatMap((v) =>
      getObjValues(v)
    );
  }
  return [o];
}