ncjamieson

RxJS: How to Use Lettable Operators and Promises

October 03, 2017

Glove on a picket
Photo by Gary Bendig on Unsplash

With the release of RxJS 5.5.0-beta.5, toPromise is now attached to Observable.prototype, rendering this look at its ‘lettable’ variant a historical curiosity.


Converting observables to promises is an antipattern. Unless you are integrating observables with a promise-based API, there is no reason to convert an observable into a promise.

If you are integrating with such an API, in his article RxJS Observable interop with Promises and Async-Await, Ben Lesh shows how it can be done. However, that article uses the prototype-patched toPromise method. Let’s have a look at the how the ‘lettable’ toPromise function — introduced in the RxJS 5.5.0 beta — could be used.

With toPromise, ‘lettable’ is something of a misnomer, as it cannot be used with the let operator. Lettable operators are higher-order functions that return functions that receive and return observables. The toPromise higher-order function returns a function that receives an observable, but it returns a promise — so it’s not lettable.

To convert an observable to a promise, toPromise needs to be called and the observable needs to be passed to the function it returns, like this:

import { range } from "rxjs/observable/range";
import { map, filter, scan, toArray, toPromise } from "rxjs/operators";

const value$ = range(0, 10).pipe(
  filter(x => x % 2 === 0),
  map(x => x + x),
  scan((acc, x) => acc + x, 0),
  toArray()
);

const value = toPromise()(value$);
value.then(x => console.log(x)); // [0, 4, 12, 24, 40]

The toPromise call cannot be placed within the call to pipe, as pipe will only accept functions that receive and return observables. Instead, it must be called separately.

Usually, toPromise would be called without an argument. However, if a particular promise implementation is to be used, a constructor can be passed. For example, a Bluebird promise could used, like this:

import { Promise as Bluebird } from "bluebird";
import { of } from "rxjs/observable/of";
import { toPromise } from "rxjs/operators";

const value$ = of("foo");
const value = toPromise(Bluebird as any)(value$);
value.then(x => console.log(x)); // "foo"

An any assertion is required because Bluebird does not have a Symbol.species property, making TypeScript deem it incompatible with typeof Promise.

If the TC39 pipeline operator proposal is eventually accepted into the ECMAScript standard, there will be an alternative, arguably-more-natural syntax, that looks like this:

import { range } from "rxjs/observable/range";
import { map, filter, scan, toArray } from "rxjs/operators";

const value = range(0, 10)
  |> filter(x => x % 2 === 0)
  |> map(x => x + x)
  |> scan((acc, x) => acc + x, 0)
  |> toArray()
  |> toPromise();

value.then(x => console.log(x)); // [0, 4, 12, 24, 40]