How to define records with fields of different types for options objects in bindings?

Hi,

Let’s say we want to define a record for a binding of a TypeScript function that expects an options object that has the optional field data which can be string | Serializable – how would we define a record or another appropriate type like that in ReScript?

Polymorphic variants ([#String(string) | #Json(Js.Json.t)) can’t be used, because they result in the wrong runtime representation.

@unwrap can’t be used, because it is only allowed directly within external bindings (and our options type can’t be part of the external definition, I believe).

The best I can come up with, is to define an abstract type and a external function that takes an unwrapped polymorphic variant, and returns that abstract type:

type data

external createData:
	(@unwrap [#String(string) | #Json(Js.Json.t)]) => data = "%identity"

type options = {
  data: data
}

let opt = { data: #String("foo")->createData }

But that doesn’t even work:

var opt = {
  data: {
    NAME: "String",
    VAL: "foo"
  }
};

Playground

(Btw.: is that a bug? From what I’ve read, that should work)

So how would I define a type that represents an options object as described above?

Even my workaround would pretty much suck, in my opinion (at least, if you need to bind to many such objects that have many fields that accept different types, as I have to).

I managed to get the above workaround working by defining my own identity function via %%raw:

type data

%%raw(`function identity(x) { return x }`)

external createData:
	(@unwrap [#String(string) | #Json(Js.Json.t)]) => data = "identity"

type options = {
  data: data
}

let opt = { data: #String("foo")->createData }

Output:

function identity(x) { return x }
;

var opt = {
  data: identity("foo")
};

…but I feel, that is even more awful… :laughing:

Another solution, I just found, would be to define an abstract type, and then define setters via @set:

type options = {
  // Required fields:
  foo: int,
  bar: string,
}

@set external data:
	(options, @unwrap [#String(string) | #Json(Js.Json.t)]) => unit = "data"

let opt = { foo: 42, bar: "baz" }
opt->data(#String("data"))

Playground

But that doesn’t make me exited, either… :wink:

It would be slightly better, if it was possible to create chainable setter:

@set external data2:
	(options, @unwrap [#String(string) | #Json(Js.Json.t)]) => options = "data"

@set external data3:
	(options, @unwrap [#String(string) | #Json(Js.Json.t)]) => options = "data"

let opt2 = { foo: 42, bar: "baz" }
	->data2(#String("data1"))
	->data3(#String("data2"))

(See last playground, at the end)

But that just produces invalid code (no error or warning):

var opt2 = (({
      foo: 42,
      bar: "baz"
    }).data = "data1").data = "data1";

(This also looks like a bug to me, btw.)

Abstract records don’t work, either:

@deriving(abstract)
type options = {
  @optional data: @unwrap [#String(string) | #Json(Js.Json.t)],
}

let opt = options(~data = #String("foo"), ())

Playground

I might be missing something, but I think this could work somehow in a type safe way.

The last thing I’ve tried was an External JS Object Creation Function:

@obj
external options: (
  ~data: @unwrap [#String(string) | #Json(Js.Json.t)] = ?,
  unit,
) => _ = ""

let opt = options(~data = #String("foo"), ())

Playground

But that fails with @obj label data does not support @unwrap arguments (at least there is an error, which I very much appreciate).

Does anybody have an idea how to archive the above in a better way?

Both, ergonomics of defining and using such objects are important to me.

One way would be

type data

external fromString: string => data = "%identity"
external fromJson: Js.Json.t => data = "%identity"

type options = {
  data: data
}

let opt = { data: "foo"->fromString }

Playground

2 Likes

Oh, right. Thank you, @hoichi!

Funnily enough, I’ve actually used that approach in my code already (but somehow forgot about it…).

This approach is probably the best so far (least hacky, nicer API), but I’m still interested in other ways, to archive the above (this approach is a bit verbose)

1 Like