How to bind a JS library for OCaml
Accessing JavaScript properties
Global variables
Global JavaScript variables are properties of the global object. Use Js.Unsafe.global to access them (window in browsers, globalThis in Node.js):
(* Access document *)
let doc : Dom_html.document Js.t = Js.Unsafe.global##.document
(* Read and write a custom global *)
let get_config () : config Js.t = Js.Unsafe.global##.myAppConfig
let set_config (x : config Js.t) = Js.Unsafe.global##.myAppConfig := xYou can also use Js.Unsafe.js_expr for any JavaScript expression:
let v = (Js.Unsafe.js_expr "window")##.documentBe careful: both Js.Unsafe.global and Js.Unsafe.js_expr are untyped. Verify the library documentation before writing type annotations.
Untyped property access
When a property is missing from the OCaml interface (e.g., dynamically added by a library), use Js.Unsafe.coerce for untyped access:
(* Read a property *)
let value = (Js.Unsafe.coerce obj)##.someProp
(* Write a property *)
(Js.Unsafe.coerce obj)##.someProp := vFor type-safe access, define a class type for the extended object or wrap the unsafe access in getter/setter functions.
Binding a JS object
Use OCaml class types to describe the shape of JavaScript objects. Each method declaration represents either a property or a method of the JS object.
Property types
| Type | Access | OCaml usage |
|---|---|---|
| t readonly_prop | read only | obj##.prop |
| t writeonly_prop | write only | obj##.prop := v |
| t prop | read/write | both |
| t1 -> t2 -> ... -> t meth | method | obj##meth arg1 arg2 |
| t optdef_prop | optional read | obj##.prop returns t Optdef.t |
Example
Given a JavaScript object:
{
name: "example", // read-only string
count: 42, // read-write number
greet: function(x) { ... } // method
}Bind it as:
class type myObj = object
method name : Js.js_string Js.t Js.readonly_prop
method count : int Js.prop
method greet : Js.js_string Js.t -> unit Js.meth
endMethod name mangling
OCaml method names are transformed to JavaScript names using underscore conventions. When accessing a field of an object, the name given in OCaml is transformed by: 1. Removing a leading underscore (if present) 2. Removing all characters starting from the last underscore
This enables: - Capitalized names: Use _Foo to refer to JavaScript's Foo - Reserved words: Use _type to refer to JavaScript's type - Method overloading: Use foo_int and foo_string for the same foo method
Examples
class type canvas = object
(* All three refer to the same JS method: drawImage *)
method drawImage :
imageElement Js.t -> int -> int -> unit Js.meth
method drawImage_withSize :
imageElement Js.t -> int -> int -> int -> int -> unit Js.meth
method drawImage_fromCanvas :
canvasElement Js.t -> int -> int -> unit Js.meth
endFull naming rules
class type example = object
(* All of these refer to JS field [meth] *)
method meth : ..
method meth_int : ..
method _meth_ : ..
method _meth_aa : ..
(* All of these refer to JS field [meth_a] *)
method meth_a_int : ..
method _meth_a_ : ..
method _meth_a_b : ..
(* Refer to [Meth] (capitalized) *)
method _Meth : ..
(* Refer to [_meth] (leading underscore in JS) *)
method __meth : ..
(* Refer to [_] *)
method __ : ..
endBinding constants from a class
For JavaScript constants like SomeLib.SomeClass.VALUE_A:
(* Type definition *)
class type someClass = object
method _VALUE_A : int Js.readonly_prop
method _VALUE_B : int Js.readonly_prop
end
(* Get the class object *)
let someClass : someClass Js.t =
(Js.Unsafe.js_expr "SomeLib")##._SomeClass
(* Access constants *)
let value_a = someClass##._VALUE_AType conversions
OCaml and JavaScript have different representations for basic types. Use these functions to convert between them.
Strings
OCaml strings and JavaScript strings have different encodings. Use Js.string and Js.to_string for UTF-8 encoded OCaml strings:
(* OCaml string -> JS string *)
let js_str : Js.js_string Js.t = Js.string "Hello"
(* JS string -> OCaml string *)
let ocaml_str : string = Js.to_string js_strFor binary data (bytes 0-255), use Js.bytestring and Js.to_bytestring:
let js_bytes = Js.bytestring "\x00\x01\x02"
let ocaml_bytes = Js.to_bytestring js_bytesBooleans
JavaScript booleans are not OCaml booleans. Use Js.bool and Js.to_bool:
let js_true : bool Js.t = Js._true (* or Js.bool true *)
let js_false : bool Js.t = Js._false (* or Js.bool false *)
let ocaml_bool : bool = Js.to_bool js_trueNumbers
OCaml integers can be used directly. For floats, use Js.float and Js.to_float:
(* Integers work directly *)
let count : int = obj##.count
(* Floats need conversion *)
let js_num : Js.number Js.t = Js.number_of_float 3.14
let ocaml_float : float = Js.float_of_number js_numArrays
(* OCaml array -> JS array *)
let js_arr : int Js.js_array Js.t = Js.array [| 1; 2; 3 |]
(* JS array -> OCaml array *)
let ocaml_arr : int array = Js.to_array js_arrSummary table
| OCaml type | JS type | OCaml → JS | JS → OCaml |
|---|---|---|---|
| string | String | Js.string | Js.to_string |
| string (bytes) | String | Js.bytestring | Js.to_bytestring |
| bool | Boolean | Js.bool | Js.to_bool |
| int | Number | (direct) | (direct) |
| float | Number | Js.float | Js.to_float |
| 'a array | Array | Js.array | Js.to_array |
Handling null and undefined
JavaScript has two "missing value" types: null and undefined. Js_of_ocaml represents these with distinct types.
Js.Opt for nullable values (null)
Use Js.Opt for values that may be null (e.g., DOM methods that return null when an element is not found):
(* Check if null *)
let is_present = Js.Opt.test value
(* Convert to OCaml option *)
let opt : element Js.t option = Js.Opt.to_option value
(* Handle both cases *)
let result = Js.Opt.case value
(fun () -> (* null *) "not found")
(fun v -> (* has value *) process v)
(* Get value or raise exception *)
let v = Js.Opt.get value (fun () -> failwith "was null")
(* Create nullable values *)
let some_val : element Js.t Js.opt = Js.some element
let null_val : element Js.t Js.opt = Js.nullJs.Optdef for optional values (undefined)
Use Js.Optdef for values that may be undefined (e.g., optional object properties, array access beyond bounds):
(* Check if defined *)
let is_defined = Js.Optdef.test value
(* Convert to OCaml option *)
let opt : config Js.t option = Js.Optdef.to_option value
(* Handle both cases *)
let result = Js.Optdef.case value
(fun () -> (* undefined *) default_config)
(fun v -> (* defined *) v)
(* Create optdef values *)
let def_val : int Js.optdef = Js.def 42
let undef_val : int Js.optdef = Js.undefinedWhen to use which
| Scenario | Type | Example |
|---|---|---|
| DOM lookup | Js.Opt | getElementById returns null if not found |
| Optional property | Js.Optdef | Property may not exist on object |
| Array access | Js.Optdef | Js.array_get returns undefined for missing index |
| API returns null | Js.Opt | Many DOM APIs use null for "no value" |
| Feature detection | Js.Optdef | Check if method/property exists |
Combining with option types
Both modules provide to_option for idiomatic OCaml code:
let find_element id =
Dom_html.getElementById_opt id
|> Js.Opt.to_option
let () =
match find_element "myId" with
| Some el -> (* use element *)
| None -> (* not found *)Calling JS functions
There are three ways to call JavaScript functions, depending on how this should be bound.
Standalone functions with fun_call
Use Js.Unsafe.fun_call for functions where this doesn't matter:
(* Equivalent to: decodeURI(s) *)
let decodeURI (s : Js.js_string Js.t) : Js.js_string Js.t =
Js.Unsafe.fun_call
(Js.Unsafe.js_expr "decodeURI")
[| Js.Unsafe.inject s |]
(* Equivalent to: parseInt(s, 10) *)
let parseInt (s : Js.js_string Js.t) : int =
Js.Unsafe.fun_call
(Js.Unsafe.js_expr "parseInt")
[| Js.Unsafe.inject s; Js.Unsafe.inject 10 |]Methods with meth_call
Use Js.Unsafe.meth_call to call a method on an object (this is bound to the object):
(* Equivalent to: arr.slice(1, 3) *)
let slice arr start stop =
Js.Unsafe.meth_call arr "slice"
[| Js.Unsafe.inject start; Js.Unsafe.inject stop |]Functions with explicit this binding
Use Js.Unsafe.call when you need to explicitly set this:
(* Equivalent to: func.call(thisArg, arg1, arg2) *)
let result =
Js.Unsafe.call func thisArg [| Js.Unsafe.inject arg1; Js.Unsafe.inject arg2 |]See the Js_of_ocaml.Js.Unsafe module API for more details.
Passing OCaml functions to JavaScript
When JavaScript code needs to call back into OCaml (e.g., event handlers, callbacks for async operations), you must wrap OCaml functions appropriately.
Basic callbacks with Js.wrap_callback
Use Js.wrap_callback to wrap an OCaml function for JavaScript:
(* setTimeout example *)
let set_timeout f ms =
Js.Unsafe.global##setTimeout
(Js.wrap_callback f)
(Js.float ms)
let () = set_timeout (fun () -> print_endline "Hello!") 1000.Js.wrap_callback handles partial application correctly: if JavaScript calls the function with fewer arguments than expected, the result is a partially applied function.
Callbacks with this binding
Use Js.wrap_meth_callback when the callback needs access to this:
(* The first parameter receives the 'this' value *)
let callback = Js.wrap_meth_callback (fun this event ->
let target : Dom_html.element Js.t = this in
(* ... handle event ... *)
Js._true)Strict arity callbacks
For performance-critical code, or when you don't need partial application support, use Js.Unsafe.callback or Js.Unsafe.callback_with_arity:
(* Strict callback - missing args become undefined, extra args are lost *)
let strict_cb = Js.Unsafe.callback (fun x y -> x + y)
(* Explicit arity - ensures exactly 2 arguments *)
let arity_cb = Js.Unsafe.callback_with_arity 2 (fun x y -> x + y)DOM event handlers
For DOM events, use Dom.handler or Dom_html.handler which automatically wraps the function and handles the return value (returning Js._false prevents the default action):
let button = Dom_html.getElementById "myButton"
let handler = Dom_html.handler (fun event ->
Dom_html.window##alert (Js.string "Clicked!");
Js._true (* return false to prevent default *))
button##.onclick := handlerFor handlers that need access to this, use Dom.full_handler:
let handler = Dom.full_handler (fun this event ->
let element : Dom_html.element Js.t = this in
(* ... *)
Js._true)Callbacks with variable arguments
Use Js.Unsafe.callback_with_arguments when the callback receives a variable number of arguments:
let varargs_cb = Js.Unsafe.callback_with_arguments (fun args ->
let len = args##.length in
Printf.printf "Called with %d arguments\n" len)Using a JS constructor
To call a JavaScript constructor (i.e., new F(...)), first obtain the constructor (i.e. from the global object), then use new%js to instantiate it.
Basic usage
(* In .ml: get the constructor *)
let dateConstr : (int -> int -> int -> Js.date Js.t) Js.constr =
Js.Unsafe.global##._Date
(* Create an instance using new%js *)
let my_date = new%js dateConstr 2024 0 15This is equivalent to:
new Date(2024, 0, 15)Handling overloaded constructors
JavaScript constructors often accept different argument types. Since OCaml requires fixed types, define multiple bindings for each signature:
(* In .mli *)
val dateConstr : (int -> int -> int -> Js.date Js.t) Js.constr
val dateConstr_fromValue : (int -> Js.date Js.t) Js.constr
val dateConstr_fromString : (Js.js_string Js.t -> Js.date Js.t) Js.constr
(* In .ml - all point to the same JS constructor *)
let dateConstr = Js.Unsafe.global##._Date
let dateConstr_fromValue = Js.Unsafe.global##._Date
let dateConstr_fromString = Js.Unsafe.global##._DateConstructors from nested objects
For constructors not on the global object, bind them first:
let workerConstr : (Js.js_string Js.t -> worker Js.t) Js.constr =
Js.Unsafe.global##._Worker
(* For nested: someLib.SomeClass *)
let someClass = (Js.Unsafe.js_expr "someLib")##._SomeClass in
new%js someClass arg1 arg2Constructing JS objects manually
To construct a JS object without calling a constructor, use the Ppx syntax extension (recommended) or Js.Unsafe.obj.
Using the PPX syntax (recommended)
let options = object%js
val x = 3 (* read-only prop *)
val mutable y = 4 (* read/write prop *)
method greet name = Js.string ("Hello " ^ Js.to_string name)
endThis produces the equivalent of:
{ x: 3, y: 4, greet: function(name) { return "Hello " + name; } }See the Ppx documentation for more details.
Using Js.Unsafe.obj
For dynamic object construction, use Js.Unsafe.obj with an array of key-value pairs:
let options : < x: int Js.prop; y: int Js.prop > Js.t =
Js.Unsafe.obj [|
("x", Js.Unsafe.inject 3);
("y", Js.Unsafe.inject 4);
|]A common pattern is to create an empty object and set properties afterwards. This is useful when calling JavaScript functions that expect an options object with optional fields. Use writeonly_prop for the type to prevent reading unset properties (which would return undefined):
class type myOptions = object
method timeout : int Js.writeonly_prop
method retry : bool Js.t Js.writeonly_prop
end
let call_with_options () =
let options : myOptions Js.t = Js.Unsafe.obj [||] in
options##.timeout := 5000;
options##.retry := Js._true;
some_js_function optionsCheck availability of methods and properties
JavaScript APIs vary across browsers and environments. A method or property may exist in one browser but not another, or may have been added in a recent version. Use Js.Optdef to safely check for and access optional members.
Checking if a member exists
let has_method obj = Js.Optdef.test (Js.Unsafe.coerce obj)##.someMethod
let has_prop obj = Js.Optdef.test (Js.Unsafe.coerce obj)##.somePropSafely accessing optional members
Use Js.Optdef.to_option to convert to an OCaml option:
let get_optional_method obj =
Js.Optdef.to_option (Js.Unsafe.coerce obj)##.someMethod
let () =
match get_optional_method my_obj with
| Some f -> (* use the method *)
| None -> (* fallback behavior *)Use Js.Optdef.case for inline handling:
let result =
Js.Optdef.case (Js.Unsafe.coerce obj)##.optionalMethod
(fun () -> (* not defined, use fallback *) default_value)
(fun m -> (* defined, use it *) m arg1 arg2)Example: feature detection
let supports_fetch () =
Js.Optdef.test Js.Unsafe.global##.fetch
let supports_local_storage () =
Js.Optdef.test Js.Unsafe.global##.localStorageObject property with multiple types
JavaScript APIs often use union types where a property can hold values of different types (e.g., string | Node or number | null). Since OCaml requires a single type, use an opaque type with runtime type checking.
Using instanceof for object types
Use Js.instanceof to distinguish between different object types (e.g., DOM nodes, custom classes). Define an opaque type and cast functions:
(* Opaque type representing the union: Element | Text *)
type element_or_text
class type container = object
method child : element_or_text Js.t Js.prop
end
(* Cast functions using instanceof *)
let as_element (x : element_or_text Js.t) : Dom.element Js.t Js.opt =
if Js.instanceof x (Js.Unsafe.global##._Element)
then Js.some (Js.Unsafe.coerce x)
else Js.null
let as_text (x : element_or_text Js.t) : Dom.text Js.t Js.opt =
if Js.instanceof x (Js.Unsafe.global##._Text)
then Js.some (Js.Unsafe.coerce x)
else Js.null
(* Usage *)
let handle_child (container : container Js.t) =
let child = container##.child in
Js.Opt.case (as_element child)
(fun () ->
Js.Opt.case (as_text child)
(fun () -> failwith "unexpected type")
(fun text -> (* handle text node *)))
(fun elem -> (* handle element *))Using typeof for primitive types
For primitive types (string, number, boolean), use Js.typeof:
type string_or_number
let as_string (x : string_or_number Js.t) : Js.js_string Js.t Js.opt =
if Js.typeof x = Js.string "string"
then Js.some (Js.Unsafe.coerce x)
else Js.null
let as_number (x : string_or_number Js.t) : Js.number Js.t Js.opt =
if Js.typeof x = Js.string "number"
then Js.some (Js.Unsafe.coerce x)
else Js.nullConverting to OCaml variants
For cleaner OCaml code, convert the union to a variant:
type child =
| Element of Dom.element Js.t
| Text of Dom.text Js.t
let classify_child (x : element_or_text Js.t) : child =
if Js.instanceof x (Js.Unsafe.global##._Element)
then Element (Js.Unsafe.coerce x)
else Text (Js.Unsafe.coerce x)
(* Now use pattern matching *)
let handle container =
match classify_child container##.child with
| Element e -> (* work with element *)
| Text t -> (* work with text node *)Accessing JavaScript runtime values
The Jsoo_runtime.Js.runtime_value function allows direct access to JavaScript values provided by the runtime (declared with //Provides: directives in JavaScript files).
external runtime_value : string -> 'a = "caml_jsoo_runtime_value"This function is useful when you need to access JavaScript values defined by an external javascript package. and you don't want to polute the global object.
Important: The argument must be a string literal, not a variable. The compiler will reject non-literal arguments.
Example
Given a JavaScript file custom.js linked with your program:
//Provides: process
var process = "my process value";
//Provides: myConfig
var myConfig = { debug: true, version: 42 };You can access these values from OCaml:
let process_str : Js.js_string Js.t = Jsoo_runtime.Js.runtime_value "process"
let config : < debug : bool Js.t Js.prop; version : int Js.prop > Js.t =
Jsoo_runtime.Js.runtime_value "myConfig"
let () =
print_endline (Js.to_string process_str);
if Js.to_bool config##.debug then
Printf.printf "Version: %d\n" config##.version