React Markdown: Help to write bindings to heterogeneous array

How can I create bindings where the parameter is a heterogeneous array

JS

import remarkGfm from 'remark-gfm'

const markdown = `A paragraph with *emphasis* and **strong importance**`.

<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
@module("remark-gfm") external remarkGfm: mdxPlugin = "default"

module ReactMarkdown = {
  @module("react-markdown") @react.component
  external make: (
    ~children: string,
    ~remarkPlugins: array<mdxPlugin>=?,
  ) => React.element = "default"
}

But in some cases I need to use the plugin with some options, i.e array<array<(mdxPlugin, options)>>

Example JS

<Markdown remarkPlugins={[[remarkGfm, {singleTilde: false}]]}>
  {markdown}
</Markdown>

Any tips using rescript v11?

1 Like

I think there are a couple of ways to do this. First off, I would say this is a case for functors - except that it isn’t because you’d have to write an unreasonable amount of bindings for the plugins.

If it wasn’t plugins but plain-old-data, you could probably use array<Js.Json.t>.

I think your only option here is to say remarkPlugins={[/*plugins here*/]->Obj.magic}

Which actually kind of sucks, and this is something I find myself doing way too often. It’s not actually a problem with ReScript, but with JS legacy. These horrible quirks you keep running into with ReScript is basically the insanity that is JS being exposed. It sucks, but you have to put up with it, or move to Rust/OCaml. There’s no way to process a heterogenous array of elements in a statically typed environment unless it’s fixed length (meaning compile-time fixed length).

1 Like

This looks like a good use case for rescript v11 untagged variants!

How could it be done with with untagged variants?

You can benefit from the fact that remark plugins are functions, options are objects (I defined it as an inline record here but you could replace it with Dict.t<JSON.t> if you want to be more generic), so all variant cases are allowed to be unboxed.

We could go a step further (and safer) by allowing tuples in unboxed variants, but it’s not the case yet, so it has to be a regular array for now (which requires the definition of an additional variant).

But it works and it’s typesafe.

playground link

module Remark = {
  type io
  type processor
  type plugin = processor => io
  @unboxed
  type pluginOrOption =
    | Plugin(plugin)
    | Option({foo?: string, bar?: int})
}

@module("remark-gfm") external remarkGfm: Remark.plugin = "default"

module ReactMarkdown = {
  @unboxed
  type rec pluginSetup =
    | Plugin(Remark.processor => Remark.io)
    | WithOptions(array<Remark.pluginOrOption>)

  @module("react-markdown") @react.component
  external make: (
    ~children: string,
    ~remarkPlugins: array<pluginSetup>=?,
  ) => React.element = "default"
}

@react.component
let make = () =>
  <ReactMarkdown
    remarkPlugins=[
      Plugin(remarkGfm),
      WithOptions([Plugin(remarkGfm), Option({bar: 10})]),
    ]>
    "hello *everybody*!"
  </ReactMarkdown>
1 Like

Thanks @tsnobip !!!

1 Like

Tuples are now allowed in master (likely to be released in rc-9), so you can simplify the bindings even more and make them safer by using a tuple:

module Remark = {
  type io
  type processor
  type plugin = processor => io
  type options = {foo?: string, bar?: int}
}

@module("remark-gfm") external remarkGfm: Remark.plugin = "default"

module ReactMarkdown = {
  @unboxed
  type rec pluginSetup =
    | Plugin(Remark.processor => Remark.io)
    | WithOptions((Remark.plugin, Remark.options))

  @module("react-markdown") @react.component
  external make: (
    ~children: string,
    ~remarkPlugins: array<pluginSetup>=?,
  ) => React.element = "default"
}

@react.component
let make = () =>
  <ReactMarkdown
    remarkPlugins=[
      Plugin(remarkGfm),
      WithOptions((remarkGfm, {bar: 10})),
    ]>
    "hello *everybody*!"
  </ReactMarkdown>
4 Likes