What's the best way to bind enum in ts?

For example, if I have an enum type in TypeScript,

enum Status {
  Initialized = 0,
  Started = 1,
  Done = 2,
  Deleted = 4,
}

and I want to write their binding in ReScript. I have seen people done it using a module and variables as following

module Status = {
  type t = int
  
  @module("moduleName") @scope("Status")
  external initialized: t = "Initialized"
  
  @module("moduleName") @scope("Status")
  external started: t = "Started"
  
  @module("moduleName") @scope("Status")
  external done_: t = "Done"
  
  @module("moduleName") @scope("Status")
  external deleted: t = "Deleted"
}

which seems like a pretty neat solution except for one thing, pattern matching won’t work for these values as they’re just variables. In that case, what’s the best way to resolve this? Like a better type representation, or a better way to handle the type without using pattern matching?

1 Like

To pattern match, you’d need to create a variant type in ReScript, then provide a conversion method between the TS and ReScript type.

1 Like

You mean I’ll have to do something like this ? Or is there any other better way to do this?

type t' =
  | Initialized
  | Started
  | Done
  | Deleted

let toStatus = (status: t): option<t'> => {
  if status === initialized {
    Some(Initialized)
  } else if status === started {
    Some(Started)
  } else if status === done_ {
    Some(Done)
  } else if status === deleted {
    Some(Deleted)
  } else {
    None
  }
}

Looking into TypeScript enums, they compile to e.g.:

{
  "0": "Initialized",
  "1": "Started",
  "2": "Done",
  "4": "Deleted",
  "Initialized": 0,
  "Started": 1,
  "Done": 2,
  "Deleted": 4
} 

So in TypeScript the expression Status.Initialized has the underlying value 0, and so on. This can be modelled in ReScript using the @bs.deriving(jsConverter) attribute, which is unfortunately not documented right now in the ReScript docs. Someone has kindly deployed an old version of the docs where it was documented: https://bucklescript.netlify.app/docs/en/generate-converters-accessors#convert-between-js-integer-enum-and-bs-variant

So e.g. it would look like

module Status = {
  @bs.deriving(jsConverter)
  type t =
  | Initialized
  | Started
  | Done
  | @bs.as(4) Deleted
}

And supposing you have a getStatus function which returns a TS enum status, this can be used like e.g.

@module("somemodule") external getStatus: unit => int = ""

And then you can use the jsConverter derived conversion function to convert the int to a ReScript Status.t:

let () = switch Status.tFromJs(getStatus()) {
  | Some(Initialized) => ...
  ...

But see also the next section in the doc I linked to if you want even stronger conversion safety.

2 Likes

Yeah you can also just use switch statements like this:

module Status = {
  type t = Initialized | Started | Done | Deleted

  let fromJs = i => {
    switch i {
    | 0 => Some(Initialized)
    | 1 => Some(Started)
    | 2 => Some(Done)
    | 4 => Some(Deleted)
    | _ => None
    }
  }

  let toJs = v => {
    switch v {
    | Initialized => 0
    | Started => 1
    | Done => 2
    | Deleted => 4
    }
  }
}

Pretty straightforward and easy to understand

Edit: forgot we were handling ints instead of strings, updated the example

6 Likes

@yawaramin @ryyppy Thanks for the solutions, both of them look neat!

1 Like

@ryyppy @yawaramin How could we create an external binding which can accept a param of type enum? Accepting an int is not typesafe right.

Actually this can be done using polyvar now in the latest release

1 Like

Awesome feature.

But the issue is the repo I work in uses v8.3

  @bs.deriving(jsConverter)
  type t = NONE | @bs.as(2) LOW | @bs.as(4) MEDIUM | @bs.as(8) HIGH

  @set
  external setMultiSample: (frameBuffer, int) => unit = "multisample"
  let setMultiSample = (frameBuffer, quality) => {
    setMultiSample(frameBuffer, tToJs(quality))
  }

currently I use jsConverter and then override setMultiSample function.

Rescript now manage that by own click here to resolve.