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 @deriving
decorator for different code generation scenarios. 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).
Generate Functions & Plain Values for Variants
Use @deriving(accessors)
on a variant type to create accessor functions for its constructors.
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
RESlet 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 @deriving(accessors)
on a record type to create accessors for its record field names.
Generate Converters for JS Object and Record
Note: In ReScript >= v7 records are already compiled to JS objects.
@deriving(jsConverter)
is therefore obsolete and will generate a no-op function for compatibility instead.
Use @deriving(jsConverter)
on a record type to create convertion functions between records / JS object runtime values.
RES@deriving(jsConverter)
type coordinates = {
x: int,
y: int
};
Generates 2 functions of the following types:
RESlet 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:
RESlet 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
:
RES@module("myGame")
external jsCoordinates : coordinates = "jsCoordinates";
More Safety
The above generated functions use JS object types. You can also hide this implementation detail by making the object type abstract by using the newType
option with @deriving(jsConverter)
:
RES@deriving({jsConverter: newType})
type coordinates = {
x: int,
y: int,
}
Generates 2 functions of the following types:
RESIlet coordinatesToJs: coordinates => abs_coordinates;
let coordinatesFromJs: abs_coordinates => coordinates;
Usage
Using newType
, you've now prevented consumers from inadvertently doing the following:
RESlet 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 @deriving(jsConverter)
on a variant type to create converter functions that allow back and forth conversion between JS integer enum and ReScript variant values.
RES@deriving(jsConverter)
type fruit =
| Apple
| Orange
| Kiwi
| Watermelon;
This option causes jsConverter
to, again, generate functions of the following types:
RESIlet 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 @as(1234)
to each constructor to customize their output.
Usage
RES@deriving(jsConverter)
type fruit =
| Apple
| @as(10) Orange
| @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 @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 @deriving(jsConverter)
:
RES@deriving({jsConverter: newType})
type fruit =
| Apple
| @as(100) Kiwi
| Watermelon;
This option causes @deriving(jsConverter)
to generate functions of the following types:
RESIlet 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
RES@deriving({jsConverter: newType})
type fruit =
| Apple
| @as(100) Kiwi
| Watermelon
let opaqueValue = fruitToJs(Apple)
@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 v8.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
@as
.
Similarly as with generating int converters, use @deriving(jsConverter)
on a polymorphic variant type to create converter functions for JS string and ReScript poly variant values.
Usage
RES@deriving(jsConverter)
type fruit = [
| #Apple
| @as("miniCoconut") #Kiwi
| #Watermelon
]
let appleString = fruitToJs(#Apple); /* "Apple" */
let kiwiString = fruitToJs(#Kiwi); /* "miniCoconut" */
As in similar use-cases before, you can also use @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 @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 @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
RES@deriving(abstract)
type person = {
name: string,
age: int,
job: string,
};
@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 @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 @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 @deriving(abstract)
annotation:
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. Two examples would be {type: "foo"}
(reserved keyword in ReScript) and {"aria-checked": true}
. Choose a valid field name then use @as
to circumvent this:
Optional Labels
You can omit fields during the creation of the object:
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 @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 @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 @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:
RESlet twenty = ageGet(joe)
Alternatively, you can use the Pipe operator (->
) for a nicer-looking access syntax:
RESlet twenty = joe->ageGet
If you prefer shorter names for the getter functions, we also support a light
setting:
RES@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 @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:
RES@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:
RESlet joe = person(~name="Joe", ~age=20, ~job="teacher");
ageSet(joe, 21);
Alternatively, with the Pipe First syntax:
RESjoe->ageSet(21)
Methods
You can attach arbitrary methods onto a type (any type, as a matter of fact. Not just @deriving(abstract)
record types). See Object Method in the "Bind to JS Function" section for more infos.
Tips & Tricks
You can leverage @deriving(abstract)
for finer-grained access control.
Mutability
You can mark a field as mutable in the implementation (.res
) file, while hiding such mutability in the interface file:
RES/* test.res */
@deriving(abstract)
type cord = {
@optional mutable x: int,
y: int,
};
RESI/* test.resi */
@deriving(abstract)
type cord = {
@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:
RES@deriving(abstract)
type cord = private {
@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:
RES@deriving(abstract)
type person = {name: string}
@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:
RESmodule Person = {
@deriving(abstract)
type t = {name: string}
}
module Cat = {
@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 = {
open Person
person->nameGet->Js.String.toUpperCase
}
/* Note how we use a local `open Cat` expression to
get access to Cat's nameGet function */
let whisperCatName = {
open Cat
cat->nameGet->Js.String.toLowerCase
}
Convert External into JS Object Creation Function
Use @obj
on an external
binding to create a function that, when called, will evaluate to a JS 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:
JSvar homeRoute = {
type: "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:
RES@obj
external route: (
~\"type": string,
~path: string,
~action: list<string> => unit,
~options: {..}=?,
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 (\"type"
) that required quoting to avoid clashing with the reserved type
keyword.
Also of interest is the return type: _
, which tells ReScript to automatically infer the full type of the JS object, sparing you the hassle of writing down the type manually!
The function is called like so:
RESlet homeRoute = route(
~\"type"="GET",
~path="/",
~action=_ => Js.log("Home"),
(),
)