Book a demo

Expose Observable methods as Promises

Blog_post3_ok

You use RxJS to implement your libary but you want to let your users decide if they prefer RxJS or regular JavaScript promises.

Opinionated choices vs large adoption

When you build a frontend library meant to be used in a lot of different use cases by a lot of different developers, it is always risky to be too much opinionated about the technology you use.

For example, if you build a library interacting with an API, you will certainly need a way to manage asynchronous requests and responses. And a very good way to do that is to use RxJS, as it is very powerful and featureful. It can address complex scenarios, it is reliable and the resulting code is very readable.

Nevertheless, not everyone wants to use RxJS, its learning curve can definitely be daunting, and anyway, if 100% of your existing code is based on Promise or async / await, switching to RxJS’s Observable will be a huge pain.

At Nuclia, we use RxJS a lot. We use in our Angular apps of course, as Angular itself relies on RxJS, but we also use it in our Svelte apps, as Svelte accepts observables as a “stores” and it is very handy.

So it was a very obvious choice for us to use RxJS to implement our frontend SDK.

Obviously, it pleases all the RxJS fans, but what about the others? Even though RxJS is very popular (State of JavaScript 2021 survey reports 19% of frontend developers use it), it is still not the main way to do asynchronous programming in JavaScript. As an API company, we cannot just ignore 79% of our public, specially beginners, who could benefit a lot from the Nuclia API and who will probably prefer to use promises.

Ideally we would like to support both RxJS and promises.


Using a JavaScript Proxy

Imagine your library is somehow like that:

export class MySmartLibrary {
    doSomething(): Observable<{status: boolean}> {
      // implementation
    }
    getStuff(id: string): Observable {
      // implementation
    }
}

RxJS allows to turn any Observable into a Promise with the firstValueFrom function (note: since RxJS 7, toPromise is deprecated):

const obs = of(1);
const promise = firstValueFrom(obs);

Ok so a brutal approach could be like that:

export class MyBrutalLibrary {
    doSomething(): Observable<{status: boolean}> {
      // implementation
    }
    doSomethingAsync(): Promise<{status: boolean}> {
      return firstValueFrom(this.doSomething());
    }
    getStuff(id: string): Observable {
      // implementation
    }
    getStuffAsync(id: string): Promise {
      return firstValueFrom(this.getStuff(id));
    }
}

const myBrutalLibrary = new MyBrutalLibrary();
myBrutalLibrary.doSomethingAsync().then(console.log);

Sure… but:

  • unlike this example, your library contains probably more than 2 methods, personally I am lazy (I am an engineer, and I think laziness is the essence of engineering), maybe you are lazy too, and anyway nobody wants to write an extra method for each RxJS-based method
  • it is not developer-friendly: the developer using your library will have a bunch of methods named very similarly, and they only matters for half of them (the RxJS ones or the async ones depending on their choice)

So let’s use a JavaScript Proxy to convert our myLibrary object into a new one supporting promises.

A Proxy is a JavaScript object that can be used to intercept and transform operations on a target object.

It works like this:

const myAsyncLibrary = new Proxy(mySmartLibrary, {
  get(target, propKey) {
    const value = Reflect.get(target, propKey);
    if (typeof value === 'function') {
      return function(...args) {
        return firstValueFrom(value.bind(target)(...args));
      };
    }
    return value;
  },
});

myAsyncLibrary.doSomething().then(console.log);

Look at that! No need to duplicate all your methods returning observables, your proxy object will automatically convert them into promises.

But wait. Yes, it does work in JavaScript. What about TypeScript?


Provide the proper typings

If your users use TypeScript, you definitely want them to get proper typing checks when using your proxied async object.

And here again, laziness/engineering demands something automatic. What’s the point in implementing a proxy converting observables into promises if you have to write a type manually?

Luckily, TypeScript allows you to do that:

export type PromiseMapper = {
  [K in keyof T]: T[K] extends (...args: infer A) => Observable ? (...args: A) => Promise : T[K];
};

Ok, let’s be honest, that is a bit cryptic. Let’s analyse how it works:

  • PromiseMapper is a generic type that takes a type T and mirrors all its properties using [K in keyof T]
  • for each property, it checks if it is a function returning an observable, and if so, it returns a function returning a promise (otherwise, it returns the value of the property T[K])
  • infer V is a type variable that will be replaced by the type of the value returned by the observable returned by the given function of T, the corresponding promise will return this exact same type: Promise
  • infer A is a type variable that will be replaced by the type of the arguments of the given function of T, the corresponding function in the resulting type will take these exact same arguments (...args: A)

Good! Now you can use this generic type to type the proxied library:

const myAsyncLibrary: PromiseMapper = new Proxy(mySmartLibrary, {
    get(target, prop) {
      const value = Reflect.get(target, prop);
      if (typeof value === 'function') {
        return (...args: typeof value.arguments) => firstValueFrom(value.bind(target)(...args));
      } else {
        return value;
      }
    },
  }) as unknown as PromiseMapper;
}

myAsyncLibrary.doSomething(123).then(console.log); // TypeScript will complain about the wrong type of the argument

Note: You do need to cast the object as unknown because TypeScript assumes the type returned by Proxy(target, handler) to be the same as the target’s type.

Usage at Nuclia

This is exactly how the Nuclia SDK allows to use either an RxJS or a Promise-based knowledge box object:

const nuclia = new Nuclia(options);
nuclia.knowledgeBox.search('life, the universe, and everything').subscribe(console.log);
nuclia.asyncKnowledgeBox.search('life, the universe, and everything').then(console.log);

Curious about how to use Nuclia in your own code or project?

                                                                                                                                                           Photo by Kelly Sikkema on Unsplash

Leave a Comment

Your email address will not be published. Required fields are marked *

Related articles

Nuclia’s latest articles and updates, right in your inbox

Pick up the topics you are the most interested in, we take care of the rest!

Want to know more?

If you want to lear more and how we can help you to implement this, please use this form or join our community on Discord for technical support .

See you soon!