How would JavaScript users consume a ReScript compiled library?

I am developing a library with ReScript that is usually consumed by apps written in ReScript (mostly my own :)), but also bundles a JS file that can be included in pure JS environments to create, process and transform custom data structures.

Exports and bundling have been pretty easy to setup, but I am unsure how to deal with the JS consumption of the library. Since there are no typechecks in JS land, calling my functions with JS data becomes very unsafe. There was not much need for input validation or error handling in ReScript as the types already made sure no incorrect data can enter.

I am considering two options now:

  • a) create a JS interop module (in ReScript) where I expose all functions I want to export to the JS bundle and wrap them around validators that check if the data actually has the correct shape or otherwise throw exceptions at the user.
  • b) leave everything as is and just add some more accessor or “make” functions for my input types so that they can be used on the JS side whenever I need them to feed into a function.

Option (a) seems kind of wrong to me since the whole point of using ReScript is to have a strong type system that actually prevents invalid input in the first place, but now I have to pretend that ReScript doesn’t do its job and types never existed.

Option (b) is more appealing to me, but having to look up a specific function or variable just to get a simple boolean or numeric input for a function also seems very tedious and hard to explain to JS users who have never touched ReScript before. Of course, there would be nothing stopping them from using any kind of compatible input that may be accepted by the function even though it is actually invalid, but I think I could live with that.

Is there a “proper” way of providing an interface to JS users from a ReScript lib that they can consume safely? Maybe there is something fundamental I am missing, since this is my first compile-to-JS project.

2 Likes

I’d say the safest way is to get genType to generate idiomatic JS or TypeScript wrappers for your code. Either works, but if you’re willing to go with TypeScript, then consumers will get nice typings and make fewer errors.

Also watch out for usage of ReScript-specific runtime modules, if your library uses these (you can check the output code) then you will probably want to have a dependency on the extracted ReScript standard library, per Proposal to solve the heavy-weight bs-platform issue - #4 by Hongbo , so that your library consumers don’t need to install ReScript themselves.

2 Likes

It depends on your use case, if you only expose a couple of functions to JS users.
You can also adopt this approach: bobzhang/rescript-repack-demo: A demo to show how to package high quality ReScript libraries for JS users (github.com)

Thanks for the suggestions! I never considered GenType since I had no plans for TypeScript or Flow integration, but I didn’t realize that it can be used with vanilla JS as well, which is great. The generated code might work for me, but I have to take a closer look.

Besides Belt I don’t have any RS dependencies right now and the bundled output is already very small, but I’ll keep that in mind.

My main reason for the JS bundle for now is to be able to use it on https://observablehq.com where I like to experiment with new ideas and write interactive articles (it’s similar to Jupyter notebook, if you don’t know it), so I just need some basic safety for now, but also want to make sure that others can use my library on the JS side as well (if they really want type safety, they might as well learn ReScript and have a much better dev experience :)).

Yes, I just need to expose a couple of functions. I am using Webpack to bundle the library for es6 and commonJS and it seems like tree shaking is working fine, so I have no problems with the file size of my output. Is there another reason why your link might be interesting in my case?

Well I guess the question is rather what data structures you want to expose to the JS api users. There are some data structures (or function designs) that don’t require much wrangling. If you give some example apis / data structures you are handling, we can probably give some more useful advice!

1 Like

You’re right, that would have been helpful. Here are some examples:

(1) The simplest structure is just a variant to define 4 possible constants that I use in calculation functions and other data structures:

type t = N | U | I | M

I want to only present the actual labels (e.g. "U" instead of 1) to consumers, they should never have to see the numbers/indices underneath in the API. This is why I have show() functions so consumers would use these instead of having to decipher the raw ReScript output. I think in this case I’d provide variables like let u = 1 instead of using strings as input which would need to be converted back and forth.

(2) These constants are also part of an array structure (as a type synonym) I call “DNA”, which should only have lengths in powers of 4 and can never be empty. This type is marked private in ReScript since I want people to only create it with the make(arr) function that takes an array whose length is being validated. This works fine with pure JS as long as there is a validation check for each array member before entering make.

(3) I have a polymorphic variant that acts as a wrapper or label for list types to specify the direction of a nesting order:

type t = [#NestToL(list<Const.t>) | #NestToR(list<Const.t>)]

I am not sure if I keep the polyvariant here or maybe find a different solution, but essentially it would be important for functions to know if the input is a #NestToL or #NestToR list. This is sometimes checked with input.NAME === "NestToR" in the compiled output, but not in every case and there is no check if input.VAL is actually a list.

(4) I also have records composed of other types like:

type t = {prop1: Foo.t, prop2: Bar.t, prop3: bool}

Now it would be nice if functions that consume t could not only check if all necessary properties are provided but also if their corresponding values have the correct type/shape (primitive types like bool included). I am not sure yet if GenType provides the latter.

Then I have a more complex GADT that roughly looks like this:

type rec t<'a> =
  | Mark(expr<'a>): t<'a>
  | Val(int): t<'a>
  | Var(string): t<var>
and expr<'a> = array<t<'a>>

This one should have constructor functions like mark(expr) and val(x) for (composable) inputs, but I may have to find a solution to annotate the provided type parameter since in some functions (like getVariables() and eval()) I differentiate between t<con> and t<var>.

Since this type is recursive I imagine it would be quite cumbersome to validate it deeply in pure JS and GenType already warned me that only shallow conversion is applied in my pattern matching function.

I hope this clears things up a bit. I believe in the end there may be no way around having to write my interface module if I want to have a decent error handling for my functions in pure JS consumption.