Context and Existing Code
I am trying to "promisify" a third-party authentication library (auth0-js) written in JavaScript. Right now it uses callback functions and there are no plans to expose an async/await
friendly API. So, I'm writing a wrapper, to avoid callback hell in my own code.
Here's a callback factory function cb
which is actively used for creating Promises.
import * as a0 from "auth0-js";
export function cb<TResult>(
resolve: (reason: TResult) => void,
reject: (reason: a0.Auth0Error) => void,
): a0.Auth0Callback<TResult> {
return (error, result) => error ? reject(error) : resolve(result);
}
And here's an example of a wrapper class.
import * as a0 from "auth0-js";
import { cb } from "./cb";
export class Authentication {
/**
* Wraps the Auth0-js' `Authentication` object and exposes Promise-based methods.
* @param wa Wrapped `Authentication` object.
*/
constructor(private wa: a0.Authentication) { }
public get dbConnection(): DbConnection {
return new DBConnection(this.wa.dbConnection);
}
public buildAuthorizeUrl(options: any): string {
return this.wa.buildAuthorizeUrl(options);
}
public loginWithDefaultDirectory(options: a0.DefaultDirectoryLoginOptions): Promise<any> {
return new Promise((resolve, reject) => this.wa.loginWithDefaultDirectory(options, cb(resolve, reject)));
}
public login(options: a0.DefaultLoginOptions): Promise<any> {
return new Promise((resolve, reject) => this.wa.login(options, cb(resolve, reject)));
}
public oauthToken(options: any): Promise<any> {
return new Promise((resolve, reject) => this.wa.oauthToken(options, cb(resolve, reject)));
}
public loginWithResourceOwner(options: a0.ResourceOwnerLoginOptions): Promise<any> {
return new Promise((resolve, reject) => this.wa.loginWithResourceOwner(options, cb(resolve, reject)));
}
public getSSOData(withActiveDirectories: boolean, options: a0.DelegationOptions): Promise<any> {
return new Promise((resolve, reject) => this.wa.getSSOData(withActiveDirectories, cb(resolve, reject)));
}
public userInfo(accessToken: string): Promise<a0.Auth0UserProfile> {
return new Promise((resolve, reject) => this.wa.userInfo(accessToken, cb(resolve, reject)));
}
public delegation(options: a0.DelegationOptions): Promise<a0.Auth0DelegationToken> {
return new Promise((resolve, reject) => this.wa.delegation(options, cb(resolve, reject)));
}
public getUserCountry(): Promise<{ countryCode: string; }> {
return new Promise((resolve, reject) => this.wa.getUserCountry(cb<{ countryCode: string; }>(resolve, reject)));
}
}
Question
While I'm always open to any constructive feedback, there are a few specific aspects I'll be particularly thankful for.
- I'm a C# developer and still know too little about JavaScript/TypeScript. If you know how to make this code look more JS-idiomatic, I'm all ears. Or I can paraphrase the question as "am I going the right/common route?"
- You can see that there is a lot of structural repetition in the code that creates Promises from callbacks (e.g.
loginWithDefaultDirectory(...)
,login(...)
,oauthToken(...)
functions). Not nice. Is there a way to apply functional programming ideas here? I have a feeling that functional composition may get handy here, but I don't know how to apply it (a sign, I haven't grasp the concept yet) - Resolved
(削除) It's also easy to notice thatbuildAuthorizeUrl(...)
is a pass-through. No callbacks here. There must be an easier way to bind one object's function to another object's function, which I don't know how to do. Very similarly,get dbConnection()
accessor is a supposed to be a pass-through. (削除ここまで)
Update 1
I figured the pass-through members can be coded very easily (one line per member):
public buildAuthorizeUrl = this.wa.buildAuthorizeUrl;
So, point 3 is now out of question.
1 Answer 1
You are correct that you can avoid a lot of the structure duplication in this class. A promisify
function would help a lot. Node has a built in util.promisify
. Alternatively, you could write a simple method to apply the arguments and resolve when called. In vanilla JS this is simple enough.
function callPromised(method, ...args) {
return new Promise((resolve, reject) => {
method(...args, (error, result) => error ? reject(error) : resolve(result))
})
}
Unfortunately, this method is difficult to type correctly in TypeScript. There's a proposal here. The best we can do now is write several overloads which gets incredibly messy with only a few parameters.
function callPromised<TResult>(method: (cb: Auth0Callback<TResult>) => void): Promise<TResult>;
function callPromised<TResult, Arg1>(method: (arg1: Arg1, cb: Auth0Callback<TResult>) => void, arg1: Arg1): Promise<TResult>;
function callPromised<TResult, Arg1, Arg2>(method: (arg1: Arg1, arg2: Arg2, cb: Auth0Callback<TResult>) => void, arg1: Arg1, arg2: Arg2): Promise<TResult>;
function callPromised(method: Function, ...args: any[]): Promise<any> {
return new Promise((resolve, reject) => {
method(...args, (error, result) => error ? reject(error) : resolve(result))
})
}
The downside of this function is that this
will not be bound correctly. This could be rectified by passing in a self
parameter and would be required for your usage.
function callPromised<TResult>(method: (cb: Auth0Callback<TResult>) => void, self: any): Promise<TResult>;
function callPromised<TResult, Arg1>(method: (arg1: Arg1, cb: Auth0Callback<TResult>) => void, self: any, arg1: Arg1): Promise<TResult>;
function callPromised<TResult, Arg1, Arg2>(method: (arg1: Arg1, arg2: Arg2, cb: Auth0Callback<TResult>) => void, self: any, arg1: Arg1, arg2: Arg2): Promise<TResult>;
function callPromised(method: Function, self: any, ...args: any[]): Promise<any> {
return new Promise((resolve, reject) => {
method.call(self, ...args, (error, result) => error ? reject(error) : resolve(result))
})
}
// For example
public loginWithDefaultDirectory(options: a0.DefaultDirectoryLoginOptions): Promise<any> {
return callPromised(this.wa.loginWithDefaultDirectory, this.wa, options)
}
-
\$\begingroup\$ Thank you Gerrit0! This is a very good answer which points to the right direction. In fact, I asked another question on SO about how to achieve what you're describing. I looked at implementations in both
promisify-js
andtyped-promisify
libraries, which are similar but not the same. Unfortunately, at this point TypeScript is still missing a lot of things related to smarter type inference. This leads to -- as you very correctly noted -- ugly, messy, and quickly growing overloads which have to be expressed upfront. Thanks again for the cool answer (I wish you saw my question a bit earlier) \$\endgroup\$Igor Soloydenko– Igor Soloydenko2017年10月05日 06:31:29 +00:00Commented Oct 5, 2017 at 6:31 -
\$\begingroup\$ And here's the link to SO question: stackoverflow.com/questions/46552920/… Plus the link to the related TypeScript feature github.com/Microsoft/TypeScript/issues/5453 Plus the link to the roadmap github.com/Microsoft/TypeScript/wiki/Roadmap \$\endgroup\$Igor Soloydenko– Igor Soloydenko2017年10月05日 06:32:46 +00:00Commented Oct 5, 2017 at 6:32
Explore related questions
See similar questions with these tags.
getSSOData
use theoptions
parameter? \$\endgroup\$