文档 / 语言手册 / Generate Converters & Helpers
Edit

You are currently looking at the v6.0 - v8.2 docs (Reason v3.6 syntax edition). You can find the latest manual page here.

(These docs are equivalent to the old BuckleScript docs before the ReScript rebrand)

Generate Converters & Helpers

When using ReScript, you will sometimes come into situations where you want to

  • Automatically generate functions that convert between ReScript's internal and JS runtime values (e.g. variants).

  • Convert a record type into an abstract type with generated creation, accessor and method functions.

  • Generate some other helper functions, such as functions from record attribute names.

You can use the @bs.deriving decorator to trigger code generation. All different options and configurations will be discussed on this page.

Note: Please be aware that extensive use of code generation might make it harder to understand your programs (since the code being generated is not visible in the source code, and you just need to know what kind of functions / values a decorator generates).

Another Note: Since v8.3 you can drop the bs. prefix for all our decorators (e.g. @bs.deriving => @deriving).

Generate Functions & Plain Values for Variants

Use [@bs.deriving accessors] on a variant type to create accessor functions for its constructors.

Reason (Old Syntax)ML (Older Syntax)JS Output
[@bs.deriving accessors]
type action =
  | Click
  | Submit(string)
  | Cancel;

Variants constructors with payloads generate functions, payload-less constructors generate plain integers (the internal representation of variants).

Note:

  • The generated accessors are lower-cased.

  • You can now use these helpers on the JavaScript side! But don't rely on their actual values please.

Usage

Reason (Old Syntax)ML (Older Syntax)
let s = submit("hello"); /* gives Submit("hello") */

This is useful:

  • When you're passing the accessor function as a higher-order function (which plain variant constructors aren't).

  • When you'd like the JS side to use these values & functions opaquely and pass you back a variant constructor (since JS has no such thing).

Please note that in case you just want to pipe a payload into a constructor, you don't need to generate functions for that. Use the -> syntax instead, e.g. "test"->Submit.

Generate Field Accessors for Records

Use [@bs.deriving accessors] on a record type to create accessors for its record field names.

Reason (Old Syntax)ML (Older Syntax)JS Output
[@bs.deriving accessors]
type pet = {
  name: string,
};

let pets = [|{name: "bob"}, {name: "bob2"}|];

pets
  ->Belt.Array.map(name)
  ->Js.Array2.joinWith("&")
  ->Js.log;

Generate Converters for JS Object and Record

Note: In ReScript >= v7 records are already compiled to JS objects. [@bs.deriving jsConverter] is therefore obsolete and will generate a no-op function for compatibility instead.

Use [@bs.deriving jsConverter] on a record type to create convertion functions between records / JS object runtime values.

Reason (Old Syntax)ML (Older Syntax)
[@bs.deriving jsConverter]
type coordinates = {
  x: int,
  y: int
};

Generates 2 functions of the following types:

Reason (Old Syntax)ML (Older Syntax)
let coordinatesToJs: coordinates => {. "x": int, "y": int};

let coordinatesFromJs: {.. "x": int, "y": int} => coordinates;

Note:

  • coordinatesFromJs uses an open object type that accepts more fields, just to be more permissive.

  • The converters are shallow. They don't recursively drill into the fields and convert them. This preserves the speed and simplicity of output while satisfying 80% of use-cases.

Usage

This exports a jsCoordinates JS object (not a record!) for JS files to use:

Reason (Old Syntax)ML (Older Syntax)
let jsCoordinates = coordinatesToJs({x: 1, y: 2});

This binds to a jsCoordinates record (not a JS object!) that exists on the JS side, presumably created by JS calling the function coordinatesFromJs:

Reason (Old Syntax)ML (Older Syntax)
[@bs.module "myGame"] external jsCoordinates : coordinates = "jsCoordinates";

More Safety

The above generated functions use Js.t object types. You can also hide this implementation detail by making the object type abstract by using the newType option with [@bs.deriving jsConverter]:

Reason (Old Syntax)ML (Older Syntax)
[@bs.deriving {jsConverter: newType}]
type coordinates = {
  x: int,
  y: int
};

Generates 2 functions of the following types:

Reason (Old Syntax)ML (Older Syntax)
let coordinatesToJs: coordinates => abs_coordinates;

let coordinatesFromJs: abs_coordinates => coordinates;

Usage

Using newType, you've now prevented consumers from inadvertently doing the following:

Reason (Old Syntax)ML (Older Syntax)
let myCoordinates = {
  x: 10,
  y: 20
};
let jsCoords = coordinatesToJs(myCoordinates);

let x = jsCoords##x; /* disallowed! Don't access the object's internal details */

Same generated output. Isn't it great that types prevent invalid accesses you'd otherwise have to encode at runtime?

Generate Converters for JS Integer Enums and Variants

Use [@bs.deriving jsConverter] on a variant type to create converter functions that allow back and forth conversion between JS integer enum and ReScript variant values.

Reason (Old Syntax)ML (Older Syntax)
[@bs.deriving jsConverter]
type fruit =
  | Apple
  | Orange
  | Kiwi
  | Watermelon;

This option causes jsConverter to, again, generate functions of the following types:

Reason (Old Syntax)ML (Older Syntax)
let fruitToJs: fruit => int;

let fruitFromJs: int => option(fruit);

For fruitToJs, each fruit variant constructor would map into an integer, starting at 0, in the order they're declared.

For fruitFromJs, the return value is an option, because not every int maps to a constructor.

You can also attach a [@bs.as alternativeIntValue] to each constructor to customize their output.

Usage

Reason (Old Syntax)ML (Older Syntax)
[@bs.deriving jsConverter]
type fruit =
  | Apple
  | [@bs.as 10] Orange
  | [@bs.as 100] Kiwi
  | Watermelon;

let zero = fruitToJs(Apple); /* 0 */

switch (fruitFromJs(100)) {
| Some(Kiwi) => Js.log("this is Kiwi")
| _ => Js.log("received something wrong from the JS side")
};

Note: by using bs.as here, all subsequent number encoding changes. Apple is still 0, Orange is 10, Kiwi is 100 and Watermelon is 101!

More Safety

Similar to the JS object <-> record deriving, you can hide the fact that the JS enum are ints by using the same newType option with [@bs.deriving jsConverter]:

Reason (Old Syntax)ML (Older Syntax)
[@bs.deriving {jsConverter: newType}]
type fruit =
  | Apple
  | [@bs.as 100] Kiwi
  | Watermelon;

This option causes jsConverter to generate functions of the following types:

Reason (Old Syntax)ML (Older Syntax)
let fruitToJs: fruit => abs_fruit;

let fruitFromJs: abs_fruit => fruit;

For fruitFromJs, the return value, unlike the previous non-abstract type case, doesn't contain an option, because there's no way a bad value can be passed into it; the only creator of abs_fruit values is fruitToJs!

Usage

Reason (Old Syntax)ML (Older Syntax)
[@bs.deriving {jsConverter: newType}]
type fruit =
  | Apple
  | [@bs.as 100] Kiwi
  | Watermelon;

let opaqueValue = fruitToJs(Apple);

[@bs.module "myJSFruits"] external jsKiwi : abs_fruit = "iSwearThisIsAKiwi";
let kiwi = fruitFromJs(jsKiwi);

let error = fruitFromJs(100); /* nope, can't take a random int */

Generate Converters for JS String Enums and Polymorphic Variants

Note: Since ReScript 8.2, polymorphic variants are already compiled to strings, so this feature is getting deprecated at some point. It's currently still useful for aliasing JS output with @bs.as.

Similarly as with generating int converters, use [@bs.deriving jsConverter] on a polymorphic variant type to create converter functions for JS string and ReScript poly variant values.

Usage

Reason (Old Syntax)ML (Older Syntax)
[@bs.deriving jsConverter]
type fruit = [
  | `Apple
  | [@bs.as "miniCoconut"] `Kiwi
  | `Watermelon
];

let appleString = fruitToJs(`Apple); /* "Apple" */
let kiwiString = fruitToJs(`Kiwi); /* "miniCoconut" */

You can also use [@bs.deriving {jsConverter: newType}] to generate abstract types instead.

Convert Record Type to Abstract Record

Note: For ReScript >= v7, we recommend using plain records to compile to JS objects. This feature might still be useful for certain scenarios, but the ergonomics might be worse

Use [@bs.deriving abstract] on a record type to expand the type into a creation, and a set of getter / setter functions for fields and methods.

Usually you'd just use ReScript records to compile to JS objects of the same shape. There is still one particular use-case left where the [@bs.deriving abstract] convertion is still useful: Whenever you need compile a record with an optional field where the JS object attribute shouldn't show up in the resulting JS when undefined (e.g. {name: "Carl", age: undefined} vs {name: "Carl"}). Check the Optional Labels section for more infos on this particular scenario.

Usage Example

Reason (Old Syntax)ML (Older Syntax)
[@bs.deriving abstract]
type person = {
  name: string,
  age: int,
  job: string,
};

[@bs.val] external john : person = "john";

Note: the person type is not a record! It's a record-looking type that uses the record's syntax and type-checking. The [@bs.deriving abstract] decorator turns it into an "abstract type" (aka you don't know what the actual value's shape).

Creation

You don't have to bind to an existing person object from the JS side. You can also create such person JS object from ReScript's side.

Since [@bs.deriving abstract] turns the above person record into an abstract type, you can't directly create a person record as you would usually. This doesn't work: {name: "Joe", age: 20, job: "teacher"}.

Instead, you'd use the creation function of the same name as the record type, implicitly generated by the [@bs.deriving abstract] annotation:

Reason (Old Syntax)ML (Older Syntax)JS Output
let joe = person(~name="Joe", ~age=20, ~job="teacher")

Note how in the example above there is no JS runtime overhead.

Rename Fields

Sometimes you might be binding to a JS object with field names that are invalid in ReScript/Reason. Two examples would be {type: "foo"} (reserved keyword in ReScript/Reason) and {"aria-checked": true}. Choose a valid field name then use [@bs.as] to circumvent this:

Reason (Old Syntax)ML (Older Syntax)JS Output
[@bs.deriving abstract]
type data = {
  [@bs.as "type"] type_: string,
  [@bs.as "aria-label"] ariaLabel: string,
};

let d = data(~type_="message", ~ariaLabel="hello");

Optional Labels

You can omit fields during the creation of the object:

Reason (Old Syntax)ML (Older Syntax)JS Output
[@bs.deriving abstract]
type person = {
  [@bs.optional] name: string,
  age: int,
  job: string,
};

let joe = person(~age=20, ~job="teacher", ());

Optional values that are not defined, will not show up as an attribute in the resulting JS object. In the example above, you will see that name was omitted.

Note that the [@bs.optional] tag turned the name field optional. Merely typing name as option(string) wouldn't work.

Note: now that your creation function contains optional fields, we mandate an unlabeled () at the end to indicate that you've finished applying the function.

Accessors

Again, since [@bs.deriving abstract] hides the actual record shape, you can't access a field using e.g. joe.age. We remediate this by generating getter and setters.

Read

One getter function is generated per bs.deriving abstract record type field. In the above example, you'd get 3 functions: nameGet, ageGet, jobGet. They take in a person value and return string, int, string respectively:

Reason (Old Syntax)ML (Older Syntax)
let twenty = ageGet(joe)

Alternatively, you can use the Pipe operator (->) for a nicer-looking access syntax:

Reason (Old Syntax)ML (Older Syntax)
let twenty = joe->ageGet

If you prefer shorter names for the getter functions, we also support a light setting:

Reason (Old Syntax)ML (Older Syntax)
[@bs.deriving {abstract: light}]
type person = {
  name: string,
  age: int,
};

let joe = person(~name="Joe", ~age=20);
let joeName = name(joe);

The getter functions will now have the same names as the object fields themselves.

Write

A [@bs.deriving abstract] value is immutable by default. To mutate such value, you need to first mark one of the abstract record field as mutable, the same way you'd mark a normal record as mutable:

Reason (Old Syntax)ML (Older Syntax)
[@bs.deriving abstract]
type person = {
  name: string,
  mutable age: int,
  job: string,
};

Then, a setter of the name ageSet will be generated. Use it like so:

Reason (Old Syntax)ML (Older Syntax)
let joe = person(~name="Joe", ~age=20, ~job="teacher");
ageSet(joe, 21);

Alternatively, with the Pipe First syntax:

Reason (Old Syntax)ML (Older Syntax)
joe->ageSet(21)

Methods

You can attach arbitrary methods onto a type (any type, as a matter of fact. Not just [@bs.deriving abstract] record types). See Object Method in the "Bind to JS Function" section for more infos.

Tips & Tricks

You can leverage [@bs.deriving abstract] for finer-grained access control.

Mutability

You can mark a field as mutable in the implementation (ml/re) file, while hiding such mutability in the interface file:

Reason (Old Syntax)ML (Older Syntax)
/* test.re */
[@bs.deriving abstract]
type cord = {
  [@bs.optional] mutable x: int,
  y: int,
};
Reason (Old Syntax)ML (Older Syntax)
/* test.rei */
[@bs.deriving abstract]
type cord = {
  [@bs.optional] x: int,
  y: int,
};

Tada! Now you can mutate inside your own file as much as you want, and prevent others from doing so!

Hide the Creation Function

Mark the record as private to disable the creation function:

Reason (Old Syntax)ML (Older Syntax)
[@bs.deriving abstract]
type cord = pri {
  [@bs.optional] x: int,
  y: int,
};

The accessors are still there, but you can no longer create such data structure. Great for binding to a JS object while preventing others from creating more such object!

Use submodules to prevent naming collisions and binding shadowing

Oftentimes you will have multiple abstract types with similar attributes. Since ReScript will expand all abstract getter, setter and creation functions in the same scope where the type is defined, you will eventually run into value shadowing problems.

For example:

Reason (Old Syntax)ML (Older Syntax)
[@bs.deriving abstract]
type person = {name: string};

[@bs.deriving abstract]
type cat = {
  name: string,
  isLazy: bool,
};

let person = person(~name="Alice");

/* Error: This expression has type person but an expression was expected
   of type cat */
person->nameGet();

To get around this issue, you can use modules to group a type with its related functions and later use them via local open statements:

Reason (Old Syntax)ML (Older Syntax)
module Person = {
  [@bs.deriving abstract]
  type t = {name: string};
};

module Cat = {
  [@bs.deriving abstract]
  type t = {
    name: string,
    isLazy: bool,
  };
};

let person = Person.t(~name="Alice");
let cat = Cat.t(~name="Snowball", ~isLazy=true);

/* We can use each nameGet function separately now */
let shoutPersonName = Person.(person->nameGet->Js.String.toUpperCase);

/* Note how we use a local open Cat.([some expression]) to 
   get access to Cat's nameGet function */
let whisperCatName = Cat.(cat->nameGet->Js.String.toLowerCase);

Convert External into JS Object Creation Function

Use @bs.obj on an external binding to create a function that, when called, will evaluate to a Js.t object with fields corresponding to the function's parameter labels.

This is very handy because you can make some of those labelled parameters optional and if you don't pass them in, the output object won't include the corresponding fields. Thus you can use it to dynamically create objects with the subset of fields you need at runtime.

For example, suppose you need a JavaScript object like this:

JS
var homeRoute = { method: "GET", path: "/", action: () => console.log("Home"), // options: ... };

But only the first three fields are required; the options field is optional. You can declare the binding function like so:

Reason (Old Syntax)ML (Older Syntax)
[@bs.obj] external route: (
  ~_method:string,
  ~path:string,
  ~action:(list(string) => unit),
  ~options:Js.t({..})=?,
  unit
) => _ = "";

Note: the = "" part at the end is just a dummy placeholder, due to syntactic limitations. It serves no purpose currently.

This function has four labelled parameters (the fourth one optional), one unlabelled parameter at the end (which we mandate for functions with optional parameters, and one parameter (_method) that requires an underscore prefix to avoid confusion with the ReScript keyword method.

Also of interest is the return type: _, which tells ReScript to automatically infer the full type of the Js.t object, sparing you the hassle of writing down the type manually!

The function is called like so:

Reason (Old Syntax)ML (Older Syntax)
let homeRoute = route(~_method="GET", ~path="/", ~action=(_ => Js.log("Home")), ());