Hien's Home
View RSS feed

Subtle difference between map and pluck RxJS operators that you should know

Published on

Update 2021.11.08

  • This post is originally published on indepth.dev on 2021.01.27
  • The RxJS source code mentioned in this post is 6.5.x
  • The pluck operator is deprecated in RxJS version 7 and will be removed in version 8. Consider using map operator with optional chaining instead.

Do you think the following code snippet will give the same result?

1from(objectList).pipe(
2 map(object => object.employee),
3 map(employee => employee.address),
4 map(address => address.houseNumber)
5)
6// vs
7from(objectList).pipe(
8 pluck('employee', 'address', 'houseNumber')
9)

Well, the answer is not really the same. Let's take a closer look to see the subtle difference.

How map operator works

According to the RxJS reference , here's what map operator does:

Applies a given project function to each value emitted by the source Observable, and emits the resulting values as an Observable.

But that's not the full picture. What will happen if an error occurs in the project function? When I deep dive into the implementation of map operator, here's what I figured out.

1try {
2 result = this.project.call(this.thisArg, value, this.count++);
3} catch (err) {
4 // if error occurs, map will emit an error notification and return
5 this.destination.error(err); // <-- this line
6 return;
7}

You can see from the above implementation, if an error occurs in the project function, the map will emit an error notification and your output stream will hang on.

How pluck operator works

Here's what pluck operator does from the official docs

Maps each source value (an object) to its specified nested property.

When I read this line, there's a question popping out in my mind, what if the nested property does not exist in the object?

And here's the answer when I consult the source code of pluck operator

1export function pluck<T, R>(...properties: string[]): OperatorFunction<T, R> {
2 // if you pass pluck('employee', 'address', 'houseNumber')
3 // the length will equal to 3
4 const length = properties.length;
5 ...
6 // under the hood, pluck operator calls map operator,
7 // and passes the plucker as projection function
8 return (source: Observable<T>) => map(plucker(properties, length))(source as any);
9}

As you can see, pluck operator calls map operator behind the scene, and passes the plucker as project function. Here’s what plucker does.

1// if you call pluck('employee', 'address', 'houseNumber')
2// props will be ['employee', 'address', 'houseNumber']
3// and length will be 3
4function plucker(props: string[], length: number): (x: string) => any {
5 const mapper = (x: string) => {
6 let currentProp = x;
7 // loop through every passed properties in the list and get the nested value from object
8 for (let i = 0; i < length; i++) {
9 // if the object doesn't have the specified property, no error will be thrown...
10 const p = currentProp != null ? currentProp[props[i]] : undefined; // <--this line
11 if (p !== void 0) {
12 currentProp = p;
13 } else {
14 // ...instead, it returns undefined
15 return undefined; // <-- this line
16 }
17 }
18 return currentProp;
19 };
20
21 return mapper;
22}

So, from the above code, we can see that the pluck operator will get the nested value from an object based on the property list you provided. For example, you call pluck('employee', 'address', 'houseNumber') , it will try to get the value at object.employee.address.houseNumber, with the main difference being that it ensures null safety.

If there's no value inside a nested object, it will return undefined, and your stream will continue to the next emitted value, rather than throwing an error and stopping like a map operator does. This is the main difference between map and pluck operator.

Recap

Let me give you a concrete example to recap. Suppose I have the following input data

1const arr = [
2 {
3 employee: {
4 address: {
5 houseNumber: 1
6 }
7 }
8 },
9 {
10 employee: {
11 // notice this employee doesn't have address
12 }
13 },
14 {
15 employee: {
16 address: {
17 houseNumber: 3
18 }
19 }
20 },
21];
22
23const arr$ = interval(1000).pipe(
24 map(index => arr[index]),
25 take(3)
26);

And I have two streams of data

1const streamWithMap = arr$.pipe(
2 map(object => object.employee),
3 map(employee => employee.address),
4 map(address => address.houseNumber)
5);
6
7const streamWithPluck = arr$.pipe(
8 pluck('employee', 'address', 'houseNumber')
9);

And here's the visualization of streamWithMap and streamWithPluck accordingly

stream with map operator map-operator.gif

stream with pluck operator pluck-operator-2.gif

If I change the streamWithMap like this, the result will be the same as when I use pluck

1const streamWithMap = arr$.pipe(
2 map(object => object?.employee?.address?.houseNumber),
3);

Conclusion

Throughout the article, I have explained in detail what's the main difference between map and pluck operators in RxJS by deep diving into the implementation. I also give an example and marble diagram to illustrate this difference.

I hope you learned something new from the blog. Thanks for reading.