Docs / BuckleScript / Object-2

Object 2

There's an alternative of binding to JavaScript objects, if the previous section's bs.deriving abstract doesn't suit your needs:

  • You don't want to declare a type beforehand

  • You want your object to be "structural", e.g. your function wants to accept "any object with the field age, not just a particular object whose type definition is declared above".

Reminder of the above distinction of record vs object here.

This section describes how BuckleScript uses OCaml's object facility to achieve this other way of binding and compiling to JavaScript objects.

Pitfall

First, note that we cannot use the ordinary OCaml/Reason object type, like this:

RE
type person = { . name: string, age: int, job: string };

You can still use this feature, but this OCaml/Reason object type does not compile to a clean JavaScript object! Unfortunately, this is because OCaml/Reason objects work a bit too differently from JS objects.

Actual Solution

BuckleScript wraps the regular OCaml/Reason object type with Js.t, in order to control and track a subset of operations and types that we know would compile cleanly to JavaScript. This is how it looks like:

RE
type person = Js.t({ . name: string, age: int, job: string }); [@bs.val] external john : person = "john";

From now on, we'll call the BuckleScript interop object "Js.t object", to disambiguate it with normal object and JS object.

Because object types are used often, Reason gives it a nicer sugar. Writing {. "name": string} is syntactic sugar for Js.t({. name: string}). Note that the double quotes around the field name name are necessary. Without it, this expression becomes an OCaml object, which you probably don't want to if you're targeting JavaScript.

Accessors

Read

To access a field, use ##: let johnName = john##name.

Write

To modify a field, you need to first mark a field as mutable. By default, the Js.t object type is immutable.

RE
type person = {. [@bs.set] "age": int}; [@bs.val] external john : person = "john"; john##age #= 99;

Note: you can't use dynamic/computed keys in this paradigm.

Call

To call a method of a field, mark the function signature as [@bs.meth]:

RE
type person = {. [@bs.meth] "say": (string, string) => unit}; [@bs.val] external john : person = "john"; john##say("hey", "jude");

Why [bs.meth]? Why not just call it directly? A JS object might carry around a reference to this, and infamously, what this points to can change. OCaml/BuckleScript functions are curried by default; this means that if you intentionally curry say, by the time you fully apply it, the this context could be wrong:

RE
/* wrong */ let talkTo = john##say("hey"); let jude = talkTo("jude"); let paul = talkTo("paul");

To ensure that folks don't accidentally curry a JavaScript method, we track every method call using ## to make sure it's fully applied immediately. Under the hood, we effectively turn a function-looking call into a special bs.meth call (it only looks like a function). Annotating the type definition of say with bs.meth completes this check.

Creation

Literal

You can use [%bs.obj putAnOCamlRecordHere] DSL to create a Js.t object:

RE
let bucklescript = [%bs.obj { info: {author: "Bob"} }]; let name = bucklescript##info##author;

Because object values are used often, Reason gives it a nicer sugar: {"foo": 1}, which desugars to: [%bs.obj {foo: 1}].

Note: there's no syntax sugar for creating an empty object in OCaml nor Reason (aka this doesn't work: [%bs.obj {}]. Please use Js.Obj.empty() for that purpose.

The created object will have an inferred type, no type declaration needed! The above example will infer as:

Note: since the value has its type inferred, don't accidentally do this:

RE
type person = {. "age": int}; let jane = {"age": "hi"};

See what went wrong here? We've declared a person type, but jane is inferred as its own type, so person is ignored and no error happens! To give jane an explicit type, simply annotate it: let jane: person = .... This will then error correctly.

Function

You can declare an external 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:

RE
[@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 OCaml/Reason keyword method.

Also of interest is the return type: _, which tells BuckleScript 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:

RE
let homeRoute = route(~_method="GET", ~path="/", ~action=(_ => Js.log("Home")), ());

This generates the desired JavaScript object–but you'll notice that the options parameter was left out. As expected, the generated object won't include the options field.

Note that for more type-safety you'll probably want to constrain the _method parameter type to only the acceptable values.