How to model classes in Rescript?

I realize this is likely a big “don’t do it,” but I’m interested in writing a library which is internally written in Rescript but externally used generally by Typescript and Javascript users. As such, I think that I can present an API which is 99% purely functional, but there is one place where I’d like to present a class: an object with some state and functions which update that state. How should I think about modeling that in Rescript for TS/JS end users?

For the record, my internal implementations will all be functions operating over state as an argument, so I could loosely model this class API in JS simply as:

class MyClass {
  constructor() {
    this.state = { ... stuff ... };
  }

  doStuff() {
    // Call to some internal-only standalone function which returns new state
    this.state = doStuff(this.state);
  }
}

Thanks!

1 Like

When I write a library in rescript for js/ts users I usually reexport package’s api in the index.ts file wrapping it in a more common interface for non-rescript users. I don’t see a point to drag classes into rescript, but to add them in the “backward bindings” is perfectly fine.

An option would be to write your class in TS/JS, and I think you can use Function.prototype.bind to insert this.state into the first argument because gentype wraps functions to be uncurried.

With strict, or at least strictBindCallApply enabled in tsconfig your gentype types should just flow through the TS file and the class wrapper really shouldn’t need any changes.

(replace the declare lines with your imports)

import type { State } from "your_rescript_module.gen";

declare const doStuff: (state: State, arg1: string, arg2: number) => unknown;
declare const defaultState: State;

class MyClass {
  state = { ...defaultState };
  doStuff = doStuff.bind(undefined, this.state);
}

The short and sweet answer I think is closures, and it’s something you can do in ReScript already.

A more idiomatic API for ReScript users is using modules to hide an internally mutable structure. You can then wrap your MyThing.make() ReScript function in a class based interface with just a little boilerplate on the JS side.

Honestly though I would skip the last step and expose the ReScript API to JS users. It’s just a matter of aesthetics at the end of the day: doThing(instance) Vs instance.doThing(). The former is more tree-shakeable though :smirk:

1 Like

This can be a serious issue. Firebase SDK did actually migrate their APIs to “modular-style” to make it tree-shakable :joy:

https://firebase.google.com/docs/web/modular-upgrade

Thanks all, very helpful! I’m definitely open to writing this small shim layer in TS/JS itself, but aiming to keep as much of this codebase as possible in ReScript if I go ahead with this direction.

@tom-sherman would you mind elaborating on that a bit? I’m brand new to ReScript so not sure exactly what that pattern might look like

Sure! The pattern looks like this

module MutableCounter = {
 type t = {
    mutable count: int
  }

  let make = () => { count: 0 }

  let increment = counter => {
    counter.count = counter.count + 1
    counter
  }
}

Some notes:

  • MutableCounter.t can be made opaque via an interface file to make its structure private. Highly recommended with mutable structures IMO, otherwise any part of the code can mutate the structure
  • Returns the counter after mutating to allow chaining eg. counter->increment->increment
  • In this example it would be more idiomatic to define t as ref<int>, I used a record for examples sake.