title | description | canonical |
---|---|---|
Bind to Global JS Values |
JS interop with global JS values in ReScript |
/docs/manual/v11.0.0/bind-to-global-js-values |
First, make sure the value you'd like to model doesn't already exist in our provided API.
Some JS values, like setTimeout
, live in the global scope. You can bind to them like so:
<CodeTab labels={["ReScript", "JS Output"]}>
@val external setTimeout: (unit => unit, int) => float = "setTimeout"
@val external clearTimeout: float => unit = "clearTimeout"
// Empty output
(We already provide setTimeout
, clearTimeout
and others in the Core API module).
This binds to the JavaScript setTimeout
methods and the corresponding clearTimeout
. The external
's type annotation specifies that setTimeout
:
- Takes a function that accepts
unit
and returnsunit
(which on the JS side turns into a function that accepts nothing and returns nothing akaundefined
), - and an integer that specifies the duration before calling said function,
- returns a number that is the timeout's ID. This number might be big, so we're modeling it as a float rather than the 32-bit int.
The above isn't ideal. See how setTimeout
returns a float
and clearTimeout
accepts one. There's no guarantee that you're passing the float created by setTimeout
into clearTimeout
! For all we know, someone might pass it Math.random()
into the latter.
We're in a language with a great type system now! Let's leverage a popular feature to solve this problem: abstract types.
<CodeTab labels={["ReScript", "JS Output"]}>
type timerId
@val external setTimeout: (unit => unit, int) => timerId = "setTimeout"
@val external clearTimeout: timerId => unit = "clearTimeout"
let id = setTimeout(() => Console.log("hello"), 100)
clearTimeout(id)
var id = setTimeout(function (param) {
console.log("hello");
}, 100);
clearTimeout(id);
Clearly, timerId
is a type that can only be created by setTimeout
! Now we've guaranteed that clearTimeout
will be passed a valid ID. Whether it's a number under the hood is now a mere implementation detail.
Since external
s are inlined, we end up with JS output as readable as hand-written JS.
If you want to bind to a value inside a global module, e.g. Math.random
, attach a scope
to your val
external:
<CodeTab labels={["ReScript", "JS Output"]}>
@scope("Math") @val external random: unit => float = "random"
let someNumber = random()
var someNumber = Math.random();
you can bind to an arbitrarily deep object by passing a tuple to scope
:
<CodeTab labels={["ReScript", "JS Output"]}>
@val @scope(("window", "location", "ancestorOrigins"))
external length: int = "length"
// Empty output
This binds to window.location.ancestorOrigins.length
.
Global values like __filename
and __DEV__
don't always exist; you can't even model them as an option
, since the mere act of referring to them in ReScript (then compiled into JS) would trigger the usual Uncaught ReferenceError: __filename is not defined
error in e.g. the browser environment.
For these troublesome global values, ReScript provides a special approach: %external(a_single_identifier)
.
<CodeTab labels={["ReScript", "JS Output"]}>
switch %external(__DEV__) {
| Some(_) => Console.log("dev mode")
| None => Console.log("production mode")
}
var match = typeof __DEV__ === "undefined" ? undefined : __DEV__;
if (match !== undefined) {
console.log("dev mode");
} else {
console.log("production mode");
}
That first line's typeof
check won't trigger a JS ReferenceError.
Another example:
<CodeTab labels={["ReScript", "JS Output"]}>
switch %external(__filename) {
| Some(f) => Console.log(f)
| None => Console.log("non-node environment")
};
var match = typeof (__filename) === "undefined" ? undefined : (__filename);
if (match !== undefined) {
console.log(match);
} else {
console.log("non-node environment");
}