Skip to content

Understanding flatMap, mergeMap and toArray operators in RxJS with TypeScript

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

RxJS is a popular library nowadays and it’s used in a wide range of applications. And it even becomes part of popular frameworks like Angular.

It’s not unknown that the library comes with many powerful functions that can be used together to solve complex problems with only a few lines of code. A couple of days ago, I saw the following question on a developer website:

How can you use flatMap operator to process a list of data as an Observable and then process the data one by one as Observables too?

Of course, there could be several ways to solve this problem. However, we will address a solution with reactive thinking to come up with a solution that could be used in many similar problems.

The Problem

Let's suppose you're building an application that needs to handle a series of asynchronous calls to display the users and their respective addresses.

user-addresses

However, in a real-world scenario, the system can provide a RESTful service to get the list of the Users(ie. getUsers()).

users

Then, you'll need to perform another HTTP call to get the address for each of the previous users (ie. getAddress(userId)).

address-by-user-id

Let's solve this problem in a Reactive Approach using RxJS operators and TypeScript.

Define the Data Model

Let's rely on TypeScript interfaces and powerful static typing to have the model ready.

// user.ts
export interface User {
  id: number;
  name: string;
  address?: Address;
}

The User interface defines the address attribute as optional. That means it can be undefined if the user doesn't "contain" a valid address value yet.

// address.ts
export interface Address {
  country: string;
  state: string;
  city: string;
  street: string;
  zipCode: number;
}

The Address interface displays a set of attributes that represents a complete address.

The Data Example

To make testing easier, let's define a dataset for the entities we defined already in the model.

// index.t

import { Address, User } from "./address";
import { User } from "./user";

const users: User[] = [
  {
    id: 0,
    name: "Donald Mayfield"
  },
  {
    id: 1,
    name: "Jill J. Fritz"
  },
  {
    id: 2,
    name: "Terry Buttram"
  }
];

const address: Address[] = [
  {
    street: "2180 BELLFLOWER",
    country: "USA",
    state: "AL",
    city: "Madison",
    zipCode: 35064
  },
  {
    street: "845 ODOM ROAD, SUITE 200",
    country: "USA",
    state: "CA",
    city: "Los Angeles",
    zipCode: 90720
  },
  {
    street: "9025 QUEENS BLVD",
    country: "USA",
    state: "NY",
    city: "Queens",
    zipCode: 11355
  }
];

Looks like we'll need a function to get an Address given a User identifier.

// index.ts
const getAddress = (userId: number): Observable<Address> => of(address[userId]);

The previous function getAddress will return an Address as an Observable: Observable<Address>. This is possible with the use of the of operator from RxJS, which is a "Creation Operator" that converts the arguments (an Address object) to an observable sequence.

In a real-world scenario, an asynchronous operation could take a variable time according to the request. So let's add a random delay for every request to "simulate" such a situation.

// index.ts
const getAddress = (userId: number): Observable<Address> =>
  of(address[userId]).pipe(
    tap(() => console.log(`getAddress(${userId})]. Started`)),
    delay(randomDelay()),
    tap(() => console.log(`getAddress(${userId}). Finished`))
  );

If the function keyword is your friend in JavaScript, then you can write it as follows:

// index.ts
function getAddress(userId: number): Observable<Address> {
  return of(address[userId]).pipe(
    tap(() => console.log(`getAddress(${userId})]. Started`)),
    delay(randomDelay()),
    tap(() => console.log(`getAddress(${userId}). Finished`))
  );
}

Processing the Data as Observables

Since we have the data model defined, it is time to create some variables that allow to "contain" the set of users and addresses as Observables too:

// index.ts

let users$: Observable<User[]>;
let address$: Observable<Address[]>;

Next, let's assign the appropriate value to the users$variable:

users$ = of(users);

Again, the previous line is using the of operator to create an observable from the users array.

In the real world, the "users" data will come from a RESTful endpoint, a JSON file, or any other function that can perform an asynchronous call. You may expect an Observable as a result of that function or you can create it using an RxJS Operator.

Using flatMap and mergeMap Operators

Now it is time to process the set of Users and perform the getAddress(id) call for every User:

// index.ts

address$ = users$.pipe(
  // "flat" the Array: User[] -> User
  flatMap(users => users)
);

Let's describe what's happening at this point.

  • users$.pipe(). This function call provides a readable way to use RxJS operators together (flatMap operator goes first).
  • flatMap() operator allows to process the data array(User[]) which comes from the observable (Observable<User[]>). Also, we can think that this operator allows transforming the users emitted by a single observable into many observables (one per user).

So far, we're able to process every user separately. However, we're not doing anything useful yet with the new observables. Let's "map" every user and perform a new request to get their addresses:

// index.ts

address$ = users$.pipe(
  flatMap(users => users),
  // process the User and return the Address as an Observable
  mergeMap(user => {
    const address$: Observable<Address> = getAddress(user.id);
    // You can even apply other operators to address$ here...
    return address$;
  })
);
  • mergeMap() operator takes the user output from the previous operator(flatMap). The user identifier is read to perform a request to obtain the respective address. As you may understand, this operator maps each value to an Observable.

Some reasons why using the mergeMap operator in this solution:

  • Since we have a set of users, it's required to get the Address of all of them.
  • The order of the final result doesn't matter.

As a final step, you'll need to subscribe as follows.

// Subscription
address$.subscribe((address: Address[]) => {
  console.log({ address });
});

After running this code, you may see something similar to the following output:

getAddress(0)]. Started
getAddress(1)]. Started
getAddress(2)]. Started
getAddress(1). Finished
{address: {…}}
getAddress(2). Finished
{address: {…}}
getAddress(0). Finished
{address: {…}}

Surprise! We are getting one object at a time. That is totally expected since we flattened the original array and then the mergeMap operator is emitting an observable at a time.

We got an initial array as an input: how can we get an array as an output?

That would be one of your questions at this time. Let's change the code using the toArray operator:

// index.ts

address$ = users$.pipe(
  flatMap(users => users),
  mergeMap(user => {
    const address$: Observable<Address> = getAddress(user.id);
    return address$;
  }),
  // Get the values inside an array at the end
  toArray()
);

With the help of toArray operator, once the source Observable is done, it will return an array containing all the emitted values(In this case an array of Address[]).

The final result may be a JSON object as follows:

{
  "address": [
  {
    "street": "2180 BELLFLOWER",
    "country": "USA",
    "state": "AL",
    "city": "Madison",
    "zipCode": 35064
  },
  {
    "street": "845 ODOM ROAD, SUITE 200",
    "country": "USA",
    "state": "CA",
    "city": "Los Angeles",
    "zipCode": 90720
  },
  {
    "street": "9025 QUEENS BLVD",
    "country": "USA",
    "state": "NY",
    "city": "Queens",
    "zipCode": 11355
  }
]
}

flatMap vs mergeMap

According to RxJS documentation, flatMap is an alias of mergeMap. You can verify it here.

However, it's usual to find some questions about flatMap operator and doubts about how it works. Also, some time ago, people around RxJS community suggested renaming mergeMap back to flatMap.

I personally like the idea of using a flatMap operator because it resembles the Array.prototype.flatMap() function in JavaScript. I prefer to use it in that way.

Alternative Solution

Would you like to see an alternative solution to this problem? Do not miss the Understanding switchMap and forkJoin operators in RxJS article.

Demo Project

Find the complete project running in StackBlitz. Don't forget to open the browser's console to see the results.

You can follow me on Twitter and GitHub to see more about my work.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co