interopffireasonbucklescriptrescript

How do I use an unwrapped polymorphic variant [union type] in a type parameter?


The Goal: Binding to the Service Worker Cache

I'm writing a binding to let me write Service Workers in ReScript. String URLs and Requests are sometimes used interchangeably.

Where possible, I'm avoiding noise in the JS output.

What I know about [@bs.unwrap]

I know I can write a binding for something like the add method using [@bs.unwrap] like so

[@bs.send]
  external add: (cache, [@bs.unwrap] [ `Request(Request.t) | `String(string)])
  => Js.Promise.t(unit) = "add";

This is a straightforward usage.

The Problem: binding with an array of Requests and / or Strings

The addAll method, however, has a more complicated type signature. It take an array of objects, which could be an array or Requests or an array of strings or an array that has both types of items.

But as far as I know, you can't unbox a type inside a type parameter like

[@bs.send]
  external addAll: (cache,
   array([@bs.unwrap] [ `Request(Request.t) | `String(string)])
  => Js.Promise.t(unit) = "addAll";

The Question: Is this kind of binding possible to model in ReScript?

Of course it would be reasonable to just abandon the string case and use Requests or to write two separate bindings and assume I won't need an array that has both.

But now I'm just curious: Is there a way to model this kind of typing in a binding in ReScript?


Solution

  • You can use an abstract type and a set of conversion functions to "cast" values to that type, instead of the polymorphic variant:

    module Value = {
      type t;
      external request: Request.t => t = "%identity";
      external str: string => t = "%identity";
    };
    
    [@bs.send]
      external addAll: (cache, array(Value.t)) => Js.Promise.t(unit) = "addAll";
    

    Example usage:

    addAll(cache, [|
      Value.request(req),
      Value.str("foo"),
    |])
    

    or using local open for brevity:

    addAll(cache, Value.[|
      request(req),
      str("foo"),
    |])
    

    This is how Js.Json encoders work too, if you've ever wondered. And since Js.Json has decoders as well, you know it's also possible to go the other way if you ever need to. Doing so is a bit more involved though, and depends on the underlying types that have been abstracted away.

    Btw, this is taken from my BuckleScript cookbook, which also has quite a few other recipes that might come in handy in tricky situations like this.