Skip to content

18 · Modules

JavaScript and TypeScript with Node

Typed functions, Node modules and asynchronous callbacks with extern.

On this page

Import javascript or typescript from the node plugin. Signatures use Zolo types; bodies use the selected language. Both providers share embedded Node and support ordinary import aliases.

Declare a small function, group TypeScript functions, capture a Zolo value in a JavaScript template, and import node:crypto. Regular expressions and nested templates preserve their native meaning.

29-extern-node.zolo
Playground
// JavaScript and TypeScript use the same embedded Node runtime.

use plugin node::{javascript as js, typescript as ts}

extern js fn add(a: int, b: int) -> int {
    return a + b;
}

extern ts {
    fn normalize(text: str) -> str {
        return text.trim().toLowerCase();
    }

    fn root(value: float) -> float {
        return Math.sqrt(value);
    }
}

let original = "  Zolo  "
let greeting: str = extern js {
    return `Hi ${`${original.trim()}`}`;
}
let lexical: bool = extern js {
    // Braces and slashes in native literals remain inside this body.

    return /[{}\/]/.test("}") && `literal } ${original.trim()}`.endsWith("Zolo");
}

extern ts fn native_import() -> str {
    import { createHash } from "node:crypto";
    return createHash("sha256").update("Zolo").digest("hex").slice(0, 8);
}

print(add(20, 22))
print(normalize(original))
print(root(81.0))
print(greeting)
print(lexical)
print(native_import().len())

// expected:

// 42

// zolo

// 9

// Hi Zolo

// true

// 8

int and i64 arrive as bigint, preserving 64 bits. Use 1n when adding a native literal to those values. Floating-point types use number.

Transfer records, bytes and nullable arrays; pass a Zolo closure, receive a native function, and retain a TypeScript instance's state.

30-extern-node-data.zolo
Playground
use plugin node::{typescript as ts}

struct Payload { value: int, optional: int?, items: [int] }

extern ts {
    fn echo(value: Payload) -> Payload {
        if (!Object.hasOwn(value, "optional") || value.optional !== null) {
            throw new Error("Nullable DTO field was lost");
        }
        return value;
    }
    fn nullable(values: [int?]) -> [int?] { return values; }
    fn empty() -> [int] { return []; }
    fn binary() -> bytes { return Buffer.from([0, 255, 42]); }
    fn call(callback: fn(int) -> int) -> int { return callback(41n); }
    fn factory(delta: int) -> fn(int) -> int { return value => value + delta; }
    fn counter() -> Any {
        class Counter {
            value = 0n;
            add(delta: bigint): bigint { this.value += delta; return this.value; }
        }
        return new Counter();
    }
}

let copy = echo(Payload {
    value: 9223372036854775807,
    optional: nil,
    items: [7, 8, 9],
})
print(copy.value)
print(copy.optional == nil)
print(copy.items.len())
let values = nullable([7, nil, nil])
print(values.len())
print(values[1] == nil)
print(empty().len())
print(binary().len())
print(call(|value: int| value + 1))
let increment = factory(1)
print(increment(41))
let live = counter()
print(live.add(40))
live.value = 41
print(live.add(1))
print(live.value)

// expected:

// 9223372036854775807

// true

// 3

// 3

// true

// 0

// 3

// 42

// 42

// 40

// 42

// 42

Import TypeScript functions/classes, CommonJS with JSDoc, and an ESM module with top-level await. Use a class alias, an async method and a property.

31-extern-node-imports.zolo
Playground
use plugin node::{javascript as js, typescript as ts}

extern ts from "./node_fixtures/tools.ts" use {twice, later, Counter as Count}
extern js from "./node_fixtures/common.cjs" use {twiceCjs}
extern js from "./node_fixtures/esm.mjs" use {fromEsm}

extern ts fn local_offset(value: int) -> int {
    import {offset} from './node_fixtures/offset.ts';
    return value + offset;
}

extern js fn local_common(value: int) -> int {
    const {twiceCjs} = require('./node_fixtures/common.cjs');
    return twiceCjs(value);
}

extern ts mod Tools {
    const {twiceCjs} = require('./node_fixtures/common.cjs');
    export function greet(name: string): string { return `Hello ${name}`; }
    export function twice(value: bigint): bigint { return twiceCjs(value); }
}

extern js mod CommonTools {
    const {twiceCjs} = require('./node_fixtures/common.cjs');
    /** @param {bigint} value @returns {bigint} */
    export function twice(value) { return twiceCjs(value); }
}

print(twice(21))
print(await later(41))
let counter = Count(40)
print(counter.add(1))
print(await counter.addLater(1))
print(counter.value)
print(counter.total)
print(twiceCjs(21))
print(fromEsm(41))
print(Tools.greet("Zolo"))
print(Tools.twice(21))
print(CommonTools.twice(21))
print(local_offset(41))
print(local_common(21))
let value = 41
let captured: int = extern ts {
    import {offset} from './node_fixtures/offset.ts';
    return value + offset;
}
print(captured)

// expected:

// 42

// 42

// 41

// 42

// 42

// 42

// 42

// 42

// Hello Zolo

// 42

// 42

// 42

// 42

// 42

A callback returning a Future becomes a Promise and can call another async Node function. Combine calls, catch errors, cancel work, and receive a timer.

32-extern-node-async.zolo
Playground
use plugin node::{typescript as ts}
use std::async::Promise

extern ts async fn later(value: int) -> int {
    await new Promise(resolve => setTimeout(resolve, 1));
    return value + 1n;
}

extern ts async fn transform(value: int, callback: fn(int) -> Future<int>) -> int {
    await new Promise(resolve => setTimeout(resolve, 1));
    return await callback(value);
}

extern ts async fn fail() -> int {
    await new Promise(resolve => setTimeout(resolve, 1));
    throw new Error("Node failed");
}

extern ts async fn wait_for_cancel() -> int {
    await new Promise(resolve => setTimeout(resolve, 60000));
    return 0n;
}

extern ts async fn schedule(callback: fn(int) -> int) -> int {
    setTimeout(() => callback(42n), 10);
    return 42n;
}

async fn nested(value: int) -> int {
    return await later(value)
}

async fn reject(value: int) -> int {
    sleep 1ms
    panic("callback failed")
    return value
}

async fn cancel_soon(future: Future<int>) -> bool {
    sleep 5ms
    future.cancel()
    return future.is_done()
}

let values = await Promise.all([
    transform(41, nested),
    transform(42, nested),
])
print(values[0])
print(values[1])
print(try { await transform(0, reject) } catch error { "callback caught" })
print(try { await fail() } catch error { "Node caught" })
let pending = wait_for_cancel()
print(await cancel_soon(pending))
let background: [int] = []
print(await schedule(|value: int| {
    background.push(value)
    return value
}))
sleep 40ms
print(background[0])

// expected:

// 42

// 43

// callback caught

// Node caught

// true

// 42

// 42

Callbacks run on the Zolo thread. When a callback awaits another Node function, declare fn(...) -> Future<T> and await the Promise in the native body. This lets the event loop continue progressing during the call.

The experimental adapter uses the Node 26.10.0 Windows x64 shared library. Deploy node-runtime/ beside the plugin or configure ZOLO_NODE_HOME. TypeScript and its declarations are embedded in the plugin. npm packages need the application's normal dependencies. Editor queries analyze current source without executing authored code. Other systems await runtime validation.

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

enpt-br