Binding a JavaScript method with a narrow return type

I am writing bindings for Firefox WebExtension API using gen_js_api.

One of the APIs returns an object literal with no definite shape: StorageArea.get() - Mozilla | MDN

So far, I have come up with:

(* Aside: the code uses Promise module from promise_jsoo *)

module Storage : sig
  module Local : sig
    type t = private Ojs.t

    val get_all : t -> unit -> Ojs.t Promise.t [ "get"]
    val get_one : t -> string -> Ojs.t Promise.t [ "get"]
    val get_some : t -> string list -> Ojs.t Promise.t [ "get"]
    val get_some_with_defaults : t -> Ojs.t -> Ojs.t Promise.t [ "get"]
    val set : t -> Ojs.t -> unit Promise.t [ "set"]

  type t = private Ojs.t

  val local : t -> Local.t [@@js.get]

module Browser : sig
  type t = private Ojs.t

  val storage : t -> Storage.t [@@js.get]

val browser : Browser.t []

While the type of the returned object literal can be anything, the user of the API usually expects a certain type depending on the context. In my case, it is

type app_state = {
  long_click_toggle: bool;
  long_click_toggle_time: int

So far, the code like this works:

  val cast_to_app_state : Ojs.t -> app_state [@@js.cast]

  module Query : sig
    val make :
         long_click_toggle:(bool[@js "longClickToggle"])
      -> long_click_toggle_time:(int[@js "longClickToggleTime"])
      -> Ojs.t

  let promised_app_state =
  let local_storage = browser |> |> Storage.local in
  Storage.Local.get_all local_storage ()
  >>| fun result -> result |> Lib.cast_to_app_state

The above code is an abridged version to cut out unnecessary context. So it may not be 100% correct.

While this works, I am wary of the @@js.cast application. Is there a type-safe way to do this? Perhaps, constraining the binding itself with a type variable (I don’t know how though)?

1 Like

It seems like it could be error-prone to cast directly the Ojs.t to app_state if the Ojs.t could technically have any fields in it (and it seems like it can from the link you sent).

Since the local storage could be anything, but you expect those two fields to be present, instead of a direct cast, would it not be safer to validate that the fields are actually present in the object, and if they are return Ok, and if they are not then return an error, (eg (app_state, string) result, etc). You could even stick app_state type with a create function into a module and ensure that you only create valid instances.