4

When defining a reason-react binding and I want to know how I can determine a binding that accepts multiple types. For example, I have an argument ~value that should accept: string, number, array(string) or array(number). At the moment I am using option('a) but I do not think this is the cleanest approach as I would prefer to define the type explicitly. How can this be done? I have looked at bs.unwrap but I am unsure how to combine external syntax into a function signature.

module Select = {
  [@bs.module "material-ui/Select"] external reactClass : ReasonReact.reactClass = "default";
  let make =
      (
        ...
        ~menuProps: option(Js.t({..}))=?,
        ~value: option('a), /* Should be type to string, number, Array of string and Array of number */
        ~style: option(ReactDOMRe.style)=?,
        ...
        children
      ) =>
    ReasonReact.wrapJsForReason(
      ~reactClass,
      ~props=
        Js.Nullable.(
          {
            ...
            "value": from_opt(value),
            "style": from_opt(style)            
          }
        ),
      children
    );
};

As a side question, as number type is not defined in reason would my binding also have to map float and integer into numbers?

glennsl
  • 28,186
  • 12
  • 57
  • 75
user465374
  • 1,521
  • 4
  • 20
  • 39
  • Both `float` and `int` are represented as js `number`, so you can technically get away with just using `float`, but you'll probably want to take both for convenience's sake. – glennsl Nov 08 '17 at 13:31

2 Answers2

4

This is possible by using the following (inspired by https://github.com/astrada/reason-react-toolbox/).

type jsUnsafe;

external toJsUnsafe : 'a => jsUnsafe = "%identity";

let unwrapValue =
    (r: [< | `Int(int) | `IntArray(array(int)) | `String(string) | `StringArray(array(string))]) =>
  switch r {
  | `String(s) => toJsUnsafe(s)
  | `Int(i) => toJsUnsafe(i)
  | `StringArray(a) => toJsUnsafe(a)
  | `IntArray(a) => toJsUnsafe(a)
  };

let optionMap = (fn, option) =>
  switch option {
  | Some(value) => Some(fn(value))
  | None => None
  };

module Select = {
  [@bs.module "material-ui/Select"] external reactClass : ReasonReact.reactClass = "default";
  let make =
      (
        ...
        ~menuProps: option(Js.t({..}))=?,
        ~value:
          option(
            [ | `Int(int) | `IntArray(array(int)) | `String(string) | `StringArray(array(string))]
           )=?,
        ~style: option(ReactDOMRe.style)=?,
        ...
        children
      ) =>
    ReasonReact.wrapJsForReason(
      ~reactClass,
      ~props=
        Js.Nullable.(
          {
            ...
            "value": from_opt(optionMap(unwrapValue, value)),
            "style": from_opt(style)            
          }
        ),
      children
    );
};

This can be used in the following way;

<Select value=(`IntArray([|10, 20|])) />
<Select value=(`Int(10)) />

I copied toJsUnsafe from reason-react-toolbox, so I'm not entirely sure exactly what it does, I will update my answer when I find out.

The unwrapValue function takes a value which can be one of the types listed and converts it to jsUnsafe.

The type for unwrapValue allows for any of variants listed, but also allows a subset of those, for example. (It's the < before the variants that enable this).

let option = (value: option([ | `String(string) | `Int(int)])) =>
  Js.Nullable.from_opt(option_map(unwrapValue, value));
glennsl
  • 28,186
  • 12
  • 57
  • 75
  • `"%identity"` is effectively a type-cast. It will change the type of the value passed to it according to the type signature it's given, without changing the value, and so it's also optimized away and therefore incurs no run-time cost. – glennsl Nov 08 '17 at 13:27
3

Just to add to @InsidersByte's answer, since this problem isn't reason-react-specific and can be generalized:

module Value = {
  type t;
  external int : int => t = "%identity";
  external intArray : array(int) => t = "%identity";
  external string : string => t = "%identity";
  external stringArray : array(string) => t = "%identity";
};

let values : list(Value.t) = [
  Value.int(4),
  Value.stringArray([|"foo", "bar"|])
];

This solution is also self-contained inside the Value module, and incurs no overhead compared to the JavaScript equivalent since "%identity" externals are no-ops that are optimized away.

glennsl
  • 28,186
  • 12
  • 57
  • 75