Understanding Operator Imports

A problem you may have run into in the past when consuming or creating a public library that depends on RxJS is handling operator inclusion. The most predominant way to include operators in your project is to import them like below:

  1. import 'rxjs/add/operator/take';

This adds the imported operator to the Observable prototype for use throughout your project:

(Source)

  1. import { Observable } from '../../Observable';
  2. import { take } from '../../operator/take';
  3. Observable.prototype.take = take;
  4. declare module '../../Observable' {
  5. interface Observable<T> {
  6. take: typeof take;
  7. }
  8. }

This method is generally OK for private projects and modules, the issue arises when you are using these imports in say, an npm package or library to be consumed throughout your organization.

A Quick Example

To see where a problem can spring up, let’s imagine Person A is creating a public Angular component library. In this library you need a few operators so you add the typical imports:

some-public-library.ts

  1. import 'rxjs/add/operator/take';
  2. import 'rxjs/add/operator/concatMap';
  3. import 'rxjs/add/operator/switchMap';

Person B comes along and includes your library. They now have access to these operators even though they did not personally import them. Probably not a huge deal but it can be confusing. You use the library and operators, life goes on…

A month later Person A decides to update their library. They no longer need switchMap or concatMap so they remove the imports:

some-public-library.ts

  1. import 'rxjs/add/operator/take';

Person B upgrades the dependency, builds their project, which now fails. They never included switchMap or concatMap themselves, it was just working based on the inclusion of a 3rd party dependency. If you were not aware this could be an issue it may take a bit to track down.

The Solution

Instead of importing operators like:

  1. import 'rxjs/add/operator/take';

We can instead import them like:

  1. import { take } from 'rxjs/operator/take';

This keeps them off the Observable prototype and let’s us call them directly:

  1. import { take } from 'rxjs/operator/take';
  2. import { of } from 'rxjs/observable/of';
  3. take.call(
  4. of(1,2,3),
  5. 2
  6. );

This quickly gets ugly however, imagine we have a longer chain:

  1. import { take } from 'rxjs/operator/take';
  2. import { map } from 'rxjs/operator/map';
  3. import { of } from 'rxjs/observable/of';
  4. map.call(
  5. take.call(
  6. of(1,2,3),
  7. 2
  8. ),
  9. val => val + 2
  10. );

Pretty soon we have a block of code that is near impossible to understand. How can we get the best of both worlds?

Pipeable Operators

RxJS now comes with a pipe helper on Observable that alleviates the pain of not having operators on the prototype. We can take the ugly block of code from above:

  1. import { take, map } from 'rxjs/operators';
  2. import { of } from 'rxjs/observable/of';
  3. map.call(
  4. take.call(
  5. of(1,2,3),
  6. 2
  7. ),
  8. val => val + 2
  9. );

And transform it into:

  1. import { take, map } from 'rxjs/operators';
  2. import { of } from 'rxjs/observable/of';
  3. of(1,2,3)
  4. .pipe(
  5. take(2),
  6. map(val => val + 2)
  7. );

Much easier to read, right? This also has the benefit of greatly reducing the RxJS bundle size in your application. For more on this, check out Ashwin Sureshkumar’s excellent article Reduce Angular app bundle size using lettable operators.