Docs / BuckleScript / GenerateConvertersAccessors

Generate Converters

Sometimes, you might want to generate e.g. function accessors from a variant declaration, or a Js.t object + converter functions from a record definition. BuckleScript comes with a few annotations that allow you to generate those.

Functions & Plain Values for Variant

Use accessors.

RE
[@bs.deriving accessors] type action = | Click | Submit(string) | Cancel;

Variants constructors with payloads generate functions, payload-less constructors generate plain integers. 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.

  • Doesn't work with polymorphic variants yet.

Output:

JS
function submit(param_0) { return /* Submit */[param_0]; } var click = /* Click */0; var cancel = /* Cancel */1; exports.click = click; exports.submit = submit; exports.cancel = cancel;

Usage

RE
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).

Generate first-class accessors for record types.

Do you find yourself typing something like this a lot?

RE
type pet = { name: string, }; let pets = [{name: "bob"}, {name: "bob2"}]; pets |> List.map(pet => pet.name) |> String.concat("&") |> Js.log;

This call to map: List.map(pet => pet.name) could be a bit more concise. Wouldn't it be nice if it looked like this instead: List.map(name)?

If we use @bs.deriving accessors on the record constructor, we can do just that. The decorator will generate a function matching the name of each field in the record which takes the record type, and returns the field type:

RE
[@bs.deriving accessors] type pet = { name: string, }; let pets = [{name: "bob"}, {name: "bob2"}]; pets |> List.map(name) |> String.concat("&") |> Js.log;

Convert Between Js.t Object and Record

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

Use jsConverter.

RE
[@bs.deriving jsConverter] type coordinates = { x: int, y: int };

Generates 2 functions of the following types:

RE
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. If you'd like recursive conversion, upvote and subscribe to this issue: https://github.com/BuckleScript/bucklescript/issues/2294 (note: please don't leave 'I want this too!' comments, it spams all subscribers of the issue ;-)

Usage

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

RE
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:

RE
[@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 passing the newType option to the jsConverter plugin:

RE
[@bs.deriving {jsConverter: newType}] type coordinates = { x: int, y: int };

Generates 2 functions of the following types:

RE
let coordinatesToJs: coordinates => abs_coordinates; let coordinatesFromJs: abs_coordinates => coordinates;

Usage

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

RE
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?

Convert between JS Integer Enum and BS Variant

Use jsConverter.

RE
[@bs.deriving jsConverter] type fruit = | Apple | Orange | Kiwi | Watermelon;

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

RE
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

RE
[@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 passing the same newType option to the jsConverter plugin:

RE
[@bs.deriving {jsConverter: newType}] type fruit = | Apple | [@bs.as 100] Kiwi | Watermelon;

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

RE
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

RE
[@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 */

Convert between JS String Enum and BS Polymorphic Variant

Similar to previous section, except polymorphic variants are converted to string instead of int.

Usage

RE
[@bs.deriving jsConverter] type fruit = [ | `Apple | [@bs.as "miniCoconut"] `Kiwi | `Watermelon ]; let appleString = fruitToJs(`Apple); /* "Apple" */ let kiwiString = fruitToJs(`Kiwi); /* "miniCoconut" */

Deriving converters with abstract type through newType also still works.