Search code examples
javascriptffireasonbucklescript

Exporting a list of record to a JavaScript array of object without tags


Say I have the following ReasonML types:

type xEntry = {title: string};
type yEntry = {value: int};
type entry =
  | X(xEntry)
  | Y(yEntry);

and I want to export the following value to the JavaScript side:

/* Input */
let value = [
  X({title: "foo"}),
  Y({value: 123})
];

as the following structure:

/* Expected output */
[{"type": "X", "title": "foo"},
 {"type": "Y", "value": 123}]

I can almost achieve it with the following code:

[@bs.deriving abstract]
type jsXEntry = {
  [@bs.as "type"]
  type_: string,
  title: string,
};

[@bs.deriving abstract]
type jsYEntry = {
  [@bs.as "type"]
  type_: string,
  value: int,
};

type jsEntry =
  | JsX(jsXEntry)
  | JsY(jsYEntry);

let fromEntry = entry =>
  switch (entry) {
  | X(v) => JsX(jsXEntry(~type_="X", ~title=v.title))
  | Y(v) => JsY(jsYEntry(~type_="Y", ~value=v.value))
  };

let convertToJs = (entries: list(entry)): Js.Array.t(jsEntry) =>
  Array.map(fromEntry, ArrayLabels.of_list(entries));

Unfortunately (but understandably) I get the following result:

/* convertToJs(value) */
[ [ { type: 'X', title: 'foo' }, tag: 0 ],
  [ { type: 'Y', value: 123 }, tag: 1 ] ]

This result contains tagged records (a single-element array, with a tag attribute), which is not what I'm looking for.

I could write a conversion function on the JavaScript side, to get rid of that, but I would prefer to directly produce the right structure from ReasonML.

Also, I would like to avoid using %bs.raw construct if possible.

How can I produce the expected output shown at the top?


Solution

  • You could use an abstract type instead of a variant for jsEntry, then use Obj.magic to circumvent the type system and cast it to that type:

    type jsEntry;
    
    let fromEntry: entry => jsEntry = 
      fun | X(v) => Obj.magic(jsXEntry(~type_="X", ~title=v.title))
          | Y(v) => Obj.magic(jsYEntry(~type_="Y", ~value=v.value))
    ;
    

    Be careful with Obj.magic though, since circumventing the type system can easily break your code. Make sure to always annotate the types to keep type inference from giving you surprises.