Docs / BuckleScript / Object

Object

JavaScript objects are used for two major purposes:

  • As a hash map (or "dictionary"), where keys can be dynamically added/removed and where values are of the same type.

  • As a record, where fields are fixed (though still maybe sometimes optional) and where values can be of different types.

Correspondingly, BuckleScript works with JS objects in these 2 ways.

Hash Map Mode

Until recently, where JS finally got proper Map support, objects have been (ab)used as a map. If you use your JS object like this:

  • might or might not add/remove arbitrary keys

  • values might or might not be accessed using a dynamic/computed key

  • values are all of the same type

Then use our Js.Dict (for "dictionary") API to bind to that JS object! In this mode, you can do all the metaprogramming you're used to with JS objects: get all keys through Js.Dict.keys, get values through Js.Dict.values, etc.

Example

RE
/* Create a JS object ourselves */ let myMap = Js.Dict.empty(); Js.Dict.set(myMap, "Allison", 10); /* Use an existing JS object */ [@bs.val] external studentAges : Js.Dict.t(int) = "student"; switch (Js.Dict.get(studentAges, "Joe")) { | None => Js.log("Joe can't be found") | Some(age) => Js.log("Joe is " ++ string_of_int(age)) };

Output:

JS
var Js_dict = require("./stdlib/js_dict.js"); var myMap = { }; myMap["Allison"] = 10; var match = Js_dict.get(student, "Joe"); if (match !== undefined) { console.log("Joe is " + String(match)); } else { console.log("Joe can't be found"); }

Design Decisions

You can see that under the hood, a Js.Dict is simply backed by a JS object. The entire API uses nothing but ordinary BuckleScript externals and wrappers, so the whole API mostly disappears after compilation. It is very convenient when converting files over from JS to BuckleScript.

Records as Objects

Note: Requires BuckleScript >= v7

In BuckleScript, records are directly compiled into JS objects with the same shape (same attribute names). As long as your record doesn't contain any BS specific data structures (variants, lists), it will almost always compile to idiomatic JS (this includes nested records etc.):

RE
type pet = | Dog | Cat; type bloodGroup = | A | B; type person = { name: string, friends: array(string), pet: option(pet), age: int, /* let's pretend our JS expects a null here */ bloodGroup: Js.Nullable.t(bloodGroup), }; let john = { name: "John", pet: None, friends: [|"Anna", "Brad", "Shin"|], age: 20, bloodGroup: Js.Nullable.null, };

will be compiled into:

JS
var john_friends = /* array */[ "Anna", "Brad", "Shin" ]; var john_bloodGroup = null; var john = { name: "John", friends: john_friends, pet: undefined, age: 20, bloodGroup: john_bloodGroup };

Please note:

  • You will still be required to transform variants and lists, so for seamless interop, make sure to only use common data types, such as array, string, int, float, etc. Alternatively you can use genType to do automatic convertions between JS <-> BuckleScript values as well.

  • None (option) values are converted to undefined, so if your JS code distincts between null and undefined, use Js.Nullable.t instead

Rename fields - [bs.as]

There are often situations where specific record attributes need to have a different name than then resulting JS object. The most prominent example would include names like type, which are often used for "Action" based patterns in JS, but cannot be expressed in BuckleScript, since type is a keyword.

To work around that, you can use the [@bs.as] attribute to change the target name within the resulting JS object:

RE
type action = { [@bs.as "type"] _type: string, }; let action = { _type: "ADD_USER" };

will be compiled to:

JS
var action = { type: "ADD_USER" };

Mutable fields

You can also use the mutable keyword to do your side-effectual work:

RE
type person = { mutable name: string }; let p = {name: "Franz"}; p.name = "Sabine";

which will translate cleanly to:

JS
var p = { name: "Franz" }; p.name = "Sabine";

Abstract Record Mode

Note: For BuckleScript >= v7, we recommend using the plain Record as Objects mechanic. This feature might still be useful for certain scenarios, but the ergonomics might be worse

If your JS object:

  • has a known, fixed set of fields

  • might or might not contain values of different types

Then you're really using it like a "record" in most other languages. For example, think of the difference of use-case and intent between the object {"John": 10, "Allison": 20, "Jimmy": 15} and {name: "John", age: 10, job: "CEO"}. The former case would be the aforementioned "hash map mode". The latter would be "abstract record mode", which in BuckleScript is modeled with the bs.deriving abstract feature:

RE
[@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 annotation 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 BuckleScript'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:

RE
let joe = person(~name="Joe", ~age=20, ~job="teacher")

Output:

JS
var joe = { name: "Joe", age: 20, job: "teacher" };

Look ma, no runtime cost!

Rename Fields

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

RE
[@bs.deriving abstract] type data = { [@bs.as "type"] type_: string, [@bs.as "aria-label"] ariaLabel: string, }; let d = data(~type_="message", ~ariaLabel="hello");

Output:

JS
var d = { type: "message", "aria-label": "hello" };

Optional Labels

You can omit fields during the creation of the object:

RE
[@bs.deriving abstract] type person = { [@bs.optional] name: string, age: int, job: string, }; let joe = person(~age=20, ~job="teacher", ());

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:

RE
let twenty = ageGet(joe)

Alternatively, you can use the Pipe First feature in a later section for a nicer-looking access syntax:

RE
let twenty = joe->ageGet

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

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

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

RE
let joe = person(~name="Joe", ~age=20, ~job="teacher"); ageSet(joe, 21);

Alternatively, with the Pipe First syntax:

RE
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 function section later.

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:

RE
/* test.re */ [@bs.deriving abstract] type cord = { [@bs.optional] mutable x: int, y: int, };
RE
/* 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:

RE
[@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 BuckleScript 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:

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

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