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"
}
};
(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…
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"))
But that doesn’t make me exited, either…
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"), ())
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"), ())
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.