Search code examples
ffireasonbucklescriptreason-react

How to define a binding that accepts multiple types in the function signature using reason-react?


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?


Solution

  • 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));