Decorators
All available @bs.
-decorators explained with examples.
[@bs.val]
bs.val
allows you to access JS values that are available on the global scope.
For example:
RE[@bs.val] external setTimeout: (unit => unit, int) => int = "setTimeout";
setTimeout(() => Js.log("Hello"), 1000);
Tip: When BS and JS names match, you can use a ""
shorthand:
RE[@bs.val] external setTimeout : (unit => unit, int) => int = "";
[@bs.val] external clearTimeout: int => unit = "";
Let's make sure that clearTimeout
can only get timeoutID
s returned by setTimeout
, not any int
.
Abstract types to the rescue!
REtype timeoutID;
[@bs.val] external setTimeout: (unit => unit, int) => timeoutID = "";
[@bs.val] external clearTimeout: timeoutID => unit = "";
What if the value we want to access is on another global object, e.g. Math.floor
, Date.now
, or even location.origin.length
?
That's what bs.scope
is for. Check it out!
[@bs.scope]
bs.scope
is used with other attributes like bs.val
and bs.module
to access "deep" values.
For example:
RE[@bs.scope "Math"] [@bs.val] external floor: float => int = "";
[@bs.scope ("location", "origin")] [@bs.val] external originLength: string = "length";
[@bs.scope "CONSTANTS"] [@bs.module "MyModule"] external initialDays: int = "";
Js.log(floor(3.4));
Js.log(originLength);
Js.log(initialDays);
compiles to:
JSvar MyModule = require("MyModule");
console.log(Math.floor(3.4));
console.log(location.origin.length);
console.log(MyModule.CONSTANTS.initialDays);
Note: The order of bs.*
attributes doesn't matter.
[@bs.get]
bs.get
is used to get a property of an object.
For example, say you created a div
element:
REtype element;
[@bs.scope "document"] [@bs.val] external createElement: string => element = "";
let div = createElement("div");
How can we access its properties, e.g. scrollTop
?
Doing this doesn't work:
RElet top = div.scrollTop; // Error: The record field scrollTop can't be found.
That's what bs.get
is for:
RE[@bs.get] external scrollTop: element => float = "";
let top = scrollTop(div);
which compiles to:
JSvar top = div.scrollTop;
Note how we defined scrollTop
as a function that takes an element
, and BS compiled it to element.scrollTop
.
[@bs.set]
bs.set
is used to set a property of an object.
For example, say you created a div
element:
REtype element;
[@bs.scope "document"] [@bs.val] external createElement: string => element = "";
let div = createElement("div");
How can we set its properties, e.g. scrollTop
?
Doing this doesn't work:
REdiv.scrollTop = 100; // Error: The record field scrollTop can't be found.
That's what bs.set
is for:
RE[@bs.set] external setScrollTop: (element, int) => unit = "scrollTop";
setScrollTop(div, 100);
which compiles to:
JSdiv.scrollTop = 100;
Note how we defined scrollTop
as a function that takes an element
and a value
, and BS compiled it to element.scrollTop = value
.
[@bs.send]
[@bs.send]
bs.send
is used to call a function on an object.
For example, say you created a div
and a button
:
REtype element;
[@bs.scope "document"] [@bs.val] external createElement: string => element = "";
let div = createElement("div");
let button = createElement("button");
How can we append the button
to the div
?
Doing this doesn't work:
REdiv.appendChild(button); // Error: The record field appendChild can't be found.
That's what bs.send
is for:
RE[@bs.send] external appendChild: (element, element) => element = "";
appendChild(div, button);
which compiles to:
JSdiv.appendChild(button);
In general, bs.send
external looks like this:
RE[@bs.send] external fn: (obj, arg1, arg2, arg3, ... , argN) => ...;
You call fn
like this:
REfn(obj, arg1, arg2, arg3, ... , argN);
/*
or equivalently:
obj->fn(arg1, arg2, arg3, ... , argN);
*/
and BS compiles this to:
JSobj.fn(arg1, arg2, arg3, ... , argN);
What if you want the object to come after the arguments like so:
REfn(arg1, arg2, arg3, ... , argN, obj);
/*
or equivalently:
obj |> fn(arg1, arg2, arg3, ... , argN);
*/
That's what bs.send.pipe
is for.
[@bs.send.pipe]
bs.send.pipe
, like bs.send
, is used to call a function on an object.
Unlike bs.send
, bs.send.pipe
takes the object as an argument:
RE[@bs.send.pipe: obj] external fn: (arg1, arg2, arg3, ... , argN) => ...;
You call fn
like this:
REfn(arg1, arg2, arg3, ... , argN, obj);
/*
or equivalently:
obj |> fn(arg1, arg2, arg3, ... , argN);
*/
and BS compiles this to:
JSobj.fn(arg1, arg2, arg3, ... , argN);
[@bs.module]
Use bs.module
when you need to require
something.
For example:
RE[@bs.module] external uuidv4: unit => string = "uuid/v4";
Js.log(uuidv4());
compiles to:
JSvar V4 = require("uuid/v4");
console.log(V4());
By passing a string to bs.module
you can bind to a specific function in the module.
For example:
REtype t;
[@bs.module "cheerio"] external load: string => t = "";
let q = load("<h2 class='title'>Hello world</h2>");
compiles to:
JSvar Cheerio = require("cheerio");
var q = Cheerio.load("<h2 class='title'>Hello world</h2>");
Note: The string provided to bs.module
can be anything, e.g.: "./path/to/my_module"
.
Dealing with ES6 export default
Say that your project has the following my_module.js
that is compiled with Babel:
JSexport default "fancy stuff";
You'd bind to it like this:
RE[@bs.module "./path/to/my_module"] external fancyStuff: string = "default";
Js.log(fancyStuff); /* => "fancy stuff" */
This compiles to:
JSvar My_module = require("./path/to/my_module");
console.log(My_module.default);
[@bs.string]
You can use bs.string
together with polymorphic variant to constrain function's parameters.
For example:
RE[@bs.module "fs"]
external readFileSync: (
~name: string,
[@bs.string] [`utf8 | `ascii]
) => string = "";
readFileSync(~name="data.txt", `utf8);
compiles to:
JSvar Fs = require("fs");
Fs.readFileSync("data.txt", "utf8");
bs.string
compiled `utf8
to the string "utf8"
.
Now, if we try pass an unknown encoding:
REreadFileSync(~name="data.txt", `binary);
we'll get a compile error.
When your string causes a syntax error in the polymorphic variant (this can happen, for example, when the string starts with a number or contains a special character), use bs.as
:
RE[@bs.module "fs"]
external readFileSync: (
~name: string,
[@bs.string] [`utf8 | `ascii | [@bs.as "ucs-2"] `ucs_2]
) => string = "";
readFileSync(~name="data.txt", `ucs_2);
This compiles to:
JSvar Fs = require("fs");
Fs.readFileSync("data.txt", "ucs-2");
[@bs.int]
You can use bs.int
together with polymorphic variant to constrain function's parameters.
For example:
RE[@bs.val]
external log: (
~status: [@bs.int] [[@bs.as 200] `ok | [@bs.as 400] `bad_request | `unauthorized],
~message: string
) => string = "";
log(~status=`unauthorized, ~message="Not allowed");
compiles to:
JSlog(401, "Not allowed");
bs.int
compiled `unauthorized
to the number 401
.
Now, if we try pass an unknown status:
RElog(~status=`not_found, ~message="This page is not found");
we'll get a compile error.
Note how we used bs.as
to better control the numbers that bs.int
compiles to.
If we omitted all the bs.as
above, bs.int
would compile:
`ok
to0
`bad_request
to1
`unauthorized
to2
[@bs.unwrap]
Say you have the following JS function:
JSfunction padLeft(padding, str) {
if (typeof padding === "number") {
return " ".repeat(padding) + str;
}
if (typeof padding === "string") {
return padding + str;
}
throw new Error("Expected padding to be number or string");
}
Note how padding
can be either a number or a string.
Here is how you'd bind to this function:
RE[@bs.val]
external padLeft: (
[@bs.unwrap] [`Int(int) | `Str(string)],
string
) => string = "";
padLeft(`Int(7), "eleven");
padLeft(`Str("7"), "eleven");
which compiles to:
JSpadLeft(7, "eleven"); /* => " eleven" */
padLeft("7", "eleven"); /* => "7eleven" */
Note how bs.unwrap
simply strips the polymorphic variant constructor (i.e. it unwraps the wrapped value).
Now, any attempts to pass a padding that's not a number or a string, will result in compile error.
REpadLeft(true, "eleven"); /* => compile error */
padLeft(`Int(7.5), "eleven"); /* => compile error */
Alternatively, you might consider to create two separate functions that bind to the same padLeft
:
RE[@bs.val] external padLeftWithSpaces: (int, string) => string = "padLeft";
[@bs.val] external padLeftWithString: (string, string) => string = "padLeft";
padLeftWithSpaces(7, "eleven");
padLeftWithString("7", "eleven");
The compiled output is the same:
JSpadLeft(7, "eleven"); /* => " eleven" */
padLeft("7", "eleven"); /* => "7eleven" */
[@bs.new]
When you need use the JS new
operator, use bs.new
.
For example:
REtype t;
[@bs.new] external createDate: unit => t = "Date";
let date = createDate();
compiles to:
JSvar date = new Date();
When the object is not available on the global scope and needs importing, add bs.module
:
REtype t;
[@bs.module] [@bs.new] external book: unit => t = "Book";
let myBook = book();
compiles to:
JSvar Book = require("Book");
var myBook = new Book();
[@bs.deriving]
[@bs.deriving accessors]
[@bs.deriving accessors]
can be used to either generate getter functions for
a decorated record type, or to generate creator functions for constructors of a variant
type.
Hint: Since BuckleScript v7, records are compiled to plain JS objects, so
you might not need [@bs.deriving accessors]
for record types. More about
this here.
Record Accessors
The decorator can be applied to record types:
RE[@bs.deriving accessors]
type person = {
name: string,
age: int,
country: string,
};
This will create a set of getter functions in the same module scope:
RElet name: (person) => string;
let age: (person) => int;
let country: (person) => string;
More details on this can be found here.
Variant Accessors
For variant type constructors we can use the same decorator:
RE[@bs.deriving accessors]
type action =
| Add(string)
| Toggle(int)
| DeleteOne(int)
| DeleteAll;
This will create a set of creator functions for every variant constructor in the same module scope which will look something like this:
RElet add: (string) => action;
let toggle: (int) => action;
let deleteOne: (int) => action;
let deleteAll: action;
We are now able to use the creator functions safely on the JS side without relying on the internal representation of a variant constructor:
JS// Note: We assume we've imported the functions from the compiled bs.js file!
const action1 = add("Drink coffee");
const action2 = toggle(34);
/* Constructors without parameter are just a plain value*/
const action3 = deleteAll;
More details on this feature can be found here.
[@bs.deriving abstract]
Hint: Since BuckleScript v7, records are compiled to plain JS objects, so you might not need [@bs.deriving abstract]
.
More about this here.
bs.deriving abstract
is used to convert a record type to the following set of constructs:
An abstract type representing the "abstract record"
A creator function for the new abstract type, where labeled arguments represent the attributes of the record
A set of setter / getter functions for each record attribute
When using this decorator, the original record type will be removed. You'll need to use the newly created functions to create / get / set values of this type.
For example let's define a decorated record type person
:
RE[@bs.deriving abstract]
type person = {
name: string,
/* Use the mutable keyword for generating setters */
mutable age: int
};
This will expand to the following code within the same module scope:
RE/* The abstract type */
type person;
let person: (~name: string, ~age: int) => person;
/* Getters */
let nameGet: (person) => string;
let ageGet: (person) => int;
/* Setters (only mutable attributes) */
let ageSet: (person, int) => unit;
This is how we would use the generated functions:
RElet friend = person(~name="Misha", ~age=20);
Js.log(friend->nameGet); /* => "Misha" */
/* Increase age by 1 via mutation */
friend->ageSet(friend->ageGet + 1);
Js.log(friend->ageGet); /* => 21 */
/* This will NOT be possible (type is abstract now): */
let misha = { name: "Misha", age: 20 };
Refer to the Abstract Record Mode section for a more detailed explanation.
[@bs.as]
bs.as
can be used with bs.deriving abstract
or records (BuckleScript v7 and above) to rename fields. This is useful when field names are valid in JS, but not in BS.
For example:
RE[@bs.deriving abstract]
type button = {
[@bs.as "type"] type_: string,
[@bs.as "aria-label"] ariaLabel: string
};
let submitButton = button(~type_="submit", ~ariaLabel="Submit");
compiles to:
JSvar submitButton = {
type: "submit",
"aria-label": "Submit"
};
Note: Since BuckleScript 7, bs.as
can also be used in records:
REtype action = {
[@bs.as "type"] _type: string,
};
will be compiled to:
JSvar action = {
type: "ADD_USER"
};
Refer to the Records as Objects section for a detailed explanation.
[@bs.variadic]
To model a JS function that takes a variable number of arguments, and all arguments are of the same type, use bs.variadic
:
RE[@bs.scope "Math"] [@bs.val] [@bs.variadic] external max: array(int) => int = "";
Js.log(max([|5, -2, 6, 1|]));
This compiles to:
JSconsole.log(Math.max(5, -2, 6, 1));
Similarly to Function.apply()
in JS, bs.variadic
converts the arguments array in BS to separate arguments on the JS side.
If your function has mandatory arguments, you could use bs.as
to add them:
RE[@bs.val] [@bs.variadic] external warn: ([@bs.as 404] _, [@bs.as "NOT_FOUND"] _, array(string)) => unit = "log";
warn([||]);
warn([|"this", "page", "is", "not", "found"|]);
This compiles to:
JSlog(404, "NOT_FOUND");
log(404, "NOT_FOUND", "this", "page", "is", "not", "found");
[@bs.inline]
A binding marked with [@bs.inline]
will tell the compiler to copy (inline) its value in every place the binding is being used.
This is especially useful for cases where e.g. string constants can't be used as variables, but need to be present as constant values.
Suppose you have a list of allowed colors which are represented as strings in JS:
RE/* Colors.re */
[@bs.inline]
let green = "green";
[@bs.inline]
let red = "red";
You can use them in another module:
RElet allowedColors = [|Colors.green, Colors.red|];
the corresponding values get directly inlined in the JS code:
JSvar allowedColors = [
"green",
"red"
];
If you omitted the [@bs.inline] statements in the example above, the resulting JS code would look like this:
JSvar allowedColors = [
Colors.green,
Colors.red
];
which is not zero-cost. Currently bs.inline
works for string
, int
and bool
. float
cannot be inlined (yet).
A more advanced, but pretty common use-case is to inline the NODE_ENV
strings:
RE/* NodeEnv.re */
[@bs.val] [@bs.scope "process.env"] external env: string = "NODE_ENV";
[@bs.inline]
let development = "development";
[@bs.inline]
let production = "production";
Invoke them in another module:
REif (NodeEnv.env === NodeEnv.development) {
Js.log("development");
}
[@bs.meth]
If you want to call a function of a Js.t
object, you need to use bs.meth
, to avoid issues with currying.
For instance, take the following JS object:
JSfunction say (a, b) {
console.log(a, b);
};
var john = {
say
};
The shape of the john
object could be typed as follows:
REtype person = {. [@bs.meth] "say": (string, string) => unit};
[@bs.val] external john: person = "john";
and the say
method can be called with the JS object access notation (##
):
REjohn##say("hey", "jude");
which compiles to
JSjohn.say("hey", "jude");
When you forget to use [@bs.meth]
in the function signature, the type checker will error, because it is ensured by the compiler to enforce only fully applied functions in Js.t
objects.
Refer to the Object 2 section for a more detailed explanation.