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:
type person = <
name: string;
age: int;
job: string
>
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:
type person = <
name: string;
age: int;
job: string
> Js.t
external john : person = "john" [@@bs.val]
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.
type person = < age : int [@bs.set] > Js.t
external john: person = "john" [@@bs.val]
let _ = john##age #= 99
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]
:
type person = < say : string -> string -> unit [@bs.meth] > Js.t
external john: person = "john" [@@bs.val]
let _ = john##say "hey" "jude"
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:
(* wrong *)
let talkTo = john##say("hey")
let jude = talkTo "jude"
let paul = talkTo "paul"
/* 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:
let bucklescript = [%bs.obj {
info = {author = "Bob"}
}]
let name = bucklescript##info##author
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:
< info: < author: string > Js.t > Js.t
{. "info": {. "author": string}}
Note: since the value has its type inferred, don't accidentally do this:
type person = <age: int> Js.t
let jane = [%bs.obj {age = "hi"}]
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:
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:
external route :
_method:string ->
path:string ->
action:(string list -> unit) ->
?options:< .. > Js.t ->
unit ->
_ = "" [@@bs.obj]
[@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:
let homeRoute = route ~_method:"GET" ~path:"/" ~action:(fun _ -> Js.log "Home") ()
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.