JavaScript Interop
Polar compiles to ES modules, so working with JavaScript goes both ways. Polar can call JavaScript functions, implement effects in JavaScript, and be imported by JavaScript and TypeScript code.
Calling JavaScript with externs
An extern gives a JavaScript function a Polar type.
Say we have src/text.js:
export function slugify(text) {
return text.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-");
}
export function first_word(text) {
const match = text.match(/\w+/);
return match ? match[0] : null;
}
export function words(text) {
return text.split(/\s+/).filter(Boolean);
}
Declare each function in the externs zone, with its
type, the file, and the name of the export:
externs
slugify(text: String) -> String = "./text.js" slugify
first_word(text: String) -> Option<String> = "./text.js" first_word
words(text: String) -> List<String> = "./text.js" words
The path is relative to the .px file, and
polar build copies the JavaScript file into
dist/. After that, externs are called like any Polar
function: slugify("Hello, World").
Note: Polar trusts the type you write.
If slugify actually returned a number, Polar couldn't
tell. Keep externs small, and test them.
Externs are synchronous
An extern called from an ordinary function is a plain, synchronous
call. JavaScript that returns a Promise, like a network
request or a timer, belongs behind an effect, which can be
asynchronous. That's the next section.
Implementing an effect in JavaScript
A bind can point an entire effect at a JavaScript module. Each operation becomes an export with the same name:
effects
Clock in Node {
now() -> Int
wait(ms: Int) -> {}
}
binds
Clock in Node = "./clock.js"
// src/clock.js
export function now() {
return Date.now();
}
export async function wait(ms) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
Operations can be async. Polar awaits them for you, and
Polar code calling Clock.wait(50) reads as if it were
synchronous. The standard library's Dom and
Process effects are written exactly this way.
Tip: Polar only makes functions
async when they use an effect that could wait. Pure
functions compile to plain synchronous JavaScript, with no
await overhead.
How values convert
At the boundary, Polar converts values based on the declared types:
| Polar type | Polar to JavaScript | JavaScript to Polar |
|---|---|---|
Int, Float, String,
Bool
|
Unchanged | Unchanged |
{} |
Unchanged | {}, whatever was returned |
Option<T> |
None becomes null,
Some(x) becomes x
|
null and undefined become
None, anything else Some
|
List<T> |
An array | Any iterable |
Result<E, A> |
Ok(x) becomes { ok: x },
Err(e) becomes { err: e }
|
An object with an ok or err key.
Anything else is a runtime error naming the operation
|
| Records | Objects, with fields converted by these rules | The same, in reverse |
| Variants | Their tagged object form | Their tagged object form |
Conversions nest, so an Option<List<Int>>,
a Result<String, List<Int>> or a record
field holding a list convert too. With the externs above:
Option.with_default(first_word("... "), "(no words)") // "(no words)", JS returned null
List.length(words("a b c")) // 3, JS returned an array
Using Polar from JavaScript
Every exported Polar function is a named ES module export:
import { cents, add, format } from "./dist/money.js";
console.log(format(add(cents(1050), cents(99)))); // $11.49
Functions that use effects are async in JavaScript, so
await them.
TypeScript
Each module also gets a .d.ts file, so TypeScript knows
the types of everything you export:
export type Money = { cents: number };
export declare function cents(n: number): Money;
export declare function format(m: Money): string;
Effectful functions are declared as returning a
Promise. For example, the browser
start function from
Running in the Browser is declared as
start(): Promise<Record<string, never>>.