Recently, I've been trying to experiment with using ReasonML in production, however, this generally involves writing bindings to existing libraries... and unfortunately, some of these libraries expose extremely difficult to type interfaces; consider trying to wrap the following getUserInfo
function (written in typescript for illustration purposes):
type xInfo = {
info: string,
message: string
};
type xError = {
error: string,
code: number
};
type xCallback = (response: xInfo | xError) => void;
declare function getUserInfo(cb: xCallback): void;
The difficulty in trying to bind this with bucklescript and ReasonML are the following:
- There is only 1 callback and it must handle both success and failure
- ReasonML doesn't support union types
- It's a callback, not a promise
So what do?
After scouring the docs for:
I finally settled on using the "shady variant" method
First, I declare a "placeholder" type which exposes just enough for me to make a decision regarding response:
[@bs.deriving abstract]
type rawResp = {
error: option(string)
};
(recall that deriving abstract takes us into record mode and creates helper methods for us - see the guide here)
I want to eventually coerce the rawResp
type into a sensible "Either" type; perhaps something like:
type asyncResult =
| Good(xInfo)
| Fail(xError);
If rawResp
contains Some(errorMsg)
, I will consider it an error, otherwise, I will consider it a success... so in code:
let castResponse = rawResp => switch(rawResp -> errorGet) {
| Some(_) => rawResp -> convertToError -> Fail
| None => rawResp -> convertToInfo -> Good
};
To build the two convertTo*
functions, I use the "shady conversion" trick outlined here
external convertToError : rawResp -> xError = "%identity";
external convertToInfo : rawResp -> xInfo = "%identity";
Then, I can build a sensible Js.Promise.t
based binding for the getUserInfo
method:
exception AsyncError(xError);
[@bs.scope "sce"]
[@bs.val]
external _getUserInfo : rawResp => unit => unit = "getUserInfo";
type getUserInfo = unit => Js.Promise.t(xInfo);
let getUserInfo : getUserInfo = unit =>
Js.Promise.make(
(~resolve, ~reject) =>
_getUserInfo(
rawResp => switch(rawResp -> castResponse) {
| Good(xInfo) => resolve(. xInfo)
| Fail(xError) => reject(. AsyncError(xError))
}
)
);
Note: I had to use a exception AsyncError(xError)
to wrap the exception; this is because the reject
callback requires an exception object.
Note 2: the period in resolve(. xInfo)
is required due to syntax constraints with uncurrying
Note 3: I have no idea if this is the correct method for wrapping such an API; but it works, so I'll just go with this for now. Perhaps one day, some ReasonML expert can give me some advice
Update: apparently, according to @glennsl, we should avoid using a tagged union, see details here: