Push to JS an array with different types


#1

Hey, how can I make an array of items with a different subItem type?
This doesn’t work, because 'a needs to be the same for all items in the array

[@bs.deriving abstract]
type item('a) = {
  name: string,
  subItem: 'a,
};
let manyItems = [|
  item(~name="item1", ~subItem="subItem1"),
  item(~name="item2", ~subItem=false),
|];

Note that I don’t want to use this array with reason, I want to send it to a JavaScript function, so I can’t use a new type like this:


#2

If OCaml allowed an array to have elements of different types, it will be impossible for it to enforce type safety. Imagine a printItem function that takes a value of item - how does it tell whether subItem is a string, so it can apply print_string on it, or whether it is a bool so it can apply print_bool on it?

The type-safe solution is to explicitly tell the compiler that you expect the value to take on one among different possible types, and that is done through variants, as in your second example. There is no getting around it. However, you can simplify the type definition and any code that relies on that type with this:

type item = {
  name: string,
  subItem: option(string),
};
let print_item = t =>
  Js.log(
    t.name
    ++ " - "
    ++ (
      switch (t.subItem) {
      | Some(subItemName) => subItemName
      | None => "(there is no subitem)"
      }
    ),
  );

print_item({name: "A", subItem: None});
print_item({name: "B", subItem: Some("x")});

Last I checked [@bs.deriving abstract] didn’t work for variant constructors that had values inside them, and so to send this to Javascript, you’ll have to write a JSON serializer explicitly. The following code uses the bs-json library:

let toJson = t =>
  Json.Encode.object_([
    ("name", t.name |> Json.Encode.string),
    (
      "subItem",
      switch (t.subItem) {
      | Some(subItemName) => subItemName |> Json.Encode.string
      | None => false |> Json.Encode.bool
      },
    ),
  ]);

This creates a Javascript object whose values can be of different types, and you can use the object directly in your Javascript code. If you want to serialize it to text you can use Js.Json.stringify, send it over a network, and parse it with JSON.parse on the other side.


#3

Thanks for the comprehensive answer jasim

I was hoping that there is some opaque array-like data structure that translates through bucklescript to JS array, that I missed (I don’t need type-checking and the end array will not be used within reason)

I will probably end up using types and raw JS, but this seemed as too much effort for something that occurs that often in JS interop (same property name, different data types)


#4

I think you can do that in Reason actually. Type soundness is not broken until you access the values pushed to the opaque array, so you could make a module like this:

module OpaqueArray = {
  type t = array(unit);
  let make = (_a: unit) => Array.make(0, ());
  [@bs.send] external push: (t, 'a) => unit = "";
  let length = (arr : t) => int => Array.length(arr);
};

If you add an interface to hide the internal implementation of the OpaqeArray.t type, you can contain the unsafety to this tiny module, and using it in the rest of the code is completely safe.
Note the argument of push has type 'a, meaning it will accept any type. You could restrict it to your Item type specifically if you want.
I added the length function as well, since it too is safe because it doesn’t access elements.

I believe this should work for the use case you describe. But be careful only to use it in this specific situation; if you find yourself trying to implement a way to get elements out of the OpaqueArray, you should probably find another approach entirely, such as the one described by jasim above.


#5

Nice! Thanks Rasmus, this is exactly what I was looking for.
I don’t care that the elements are opaque as I only need to send the array to a 3rd party JS