uniffi-bindgen-js
Call Rust code from JavaScript and TypeScript.
uniffi-bindgen-js generates idiomatic TypeScript bindings from UniFFI interface definitions. Define your API once in Rust, compile to WebAssembly, and get typed, documented TypeScript that works in browsers, Node.js, Deno, and Bun.
Quickstart
1. Define your interface in a UDL file (src/math.udl):
namespace math {
u32 add(u32 left, u32 right);
string greet(string name);
};
2. Implement it in Rust (src/lib.rs):
Configure for UniFFI + WASM:
# Cargo.toml
[]
= ["cdylib"]
[]
= { = "0.31", = ["scaffolding-ffi-buffer-fns", "wasm-unstable-single-threaded"] }
3. Build the WASM module:
4. Generate TypeScript bindings:
# With configuration (custom namespace, rename/exclude, etc.):
5. Use it:
import { Math } from './pkg/math.js';
console.log(Math.add(2, 3)); // 5
console.log(Math.greet('World')); // "Hello, World!"
The generator reads your compiled WASM binary (or UDL file) and emits TypeScript that calls UniFFI FFI functions directly — no wasm-pack or wasm-bindgen required. The .wasm file is loaded automatically from the same directory using import.meta.url.
Install
Requires Rust.
Or build from source:
What it generates
Generated TypeScript is designed to look like something you would write by hand. Exported names use camelCase; internal FFI calls retain the original Rust snake_case names.
Top-level functions
UDL:
namespace math {
u32 add(u32 left, u32 right);
string greet(string name);
};
Generated TypeScript:
export namespace Math {
export function add(left: number, right: number): number { /* FFI call */ }
export function greet(name: string): string { /* FFI call */ }
}
Top-level functions are grouped into a namespace named after the UDL file (PascalCase).
Objects
UDL:
interface Counter {
constructor(i64 start);
void increment();
i64 get();
};
Generated TypeScript:
export class Counter {
private _freed = false;
private _assertLive(): void {
if (this._freed) throw new Error('Counter object has been freed');
}
static create(start: bigint): Counter { /* FFI call */ }
increment(): void { this._assertLive(); /* FFI call */ }
get(): bigint { this._assertLive(); /* FFI call */ }
/** Releases the underlying WASM resource. Safe to call more than once. */
free(): void {
if (this._freed) return;
this._freed = true;
_rt.unregisterPointer(this);
_rt.callFree('uniffi_counter_fn_free_counter', this._handle);
}
}
if (Symbol.dispose) (Counter as any).prototype[Symbol.dispose] = Counter.prototype.free;
Objects are wrapped in lifecycle-safe classes with FinalizationRegistry support, free() for deterministic cleanup, Symbol.dispose for using declarations, and guards against use-after-free.
Records
UDL:
dictionary Point {
f64 x;
f64 y;
};
Generated TypeScript:
export interface Point {
x: number;
y: number;
}
Enums
UDL:
enum Direction { "North", "South", "East", "West" };
[Enum]
interface Shape {
Circle(f64 radius);
Rectangle(f64 width, f64 height);
Point();
};
Generated TypeScript:
export type Direction = 'North' | 'South' | 'East' | 'West';
export type Shape =
| { tag: 'Circle', radius: number }
| { tag: 'Rectangle', width: number, height: number }
| { tag: 'Point' };
Flat enums map to string literal unions; data-carrying enums map to discriminated unions with exhaustive pattern matching.
Errors
Rust:
Generated TypeScript:
export class NetworkError extends Error {
override readonly name = 'NetworkError' as const;
constructor(public readonly variant: NetworkErrorVariant) { /* ... */ }
static NotFound(url: string): NetworkError { /* ... */ }
static Timeout(timeoutMs: number): NetworkError { /* ... */ }
}
Catching errors:
try {
MyApi.fetchData(url);
} catch (e) {
if (e instanceof NetworkError) {
console.error(e.message); // "NotFound: url=https://example.com"
console.error(e.variant.tag); // "NotFound"
if (e.variant.tag === 'NotFound') {
console.error(e.variant.url); // "https://example.com" (typed access)
}
}
}
Rich errors have human-readable .message strings built from the variant fields, structured .variant data for programmatic matching, and standard .cause for error chain tooling. Flat errors (no fields) produce a .message equal to the variant tag.
Usage
Generate command
The tool auto-detects the mode from the file extension:
- WASM mode (
.wasm) — reads metadata from a compiled WASM binary. Copies the.wasmto the output directory. This is the recommended approach. - Library mode (
.dylib/.so/.dll) — reads metadata from a compiled UniFFI cdylib. - UDL mode (
.udl) — reads a UDL file directly. Useful during development; the.wasmfile must be placed alongside the output manually.
| Flag | Description |
|---|---|
--out-dir <dir> |
Output directory for generated TypeScript files |
--config <file> |
Path to uniffi.toml configuration |
--crate <name> |
Generate bindings for this crate only (library mode) |
Configuration
Place a [bindings.js] section in your uniffi.toml and pass it with --config:
[]
= "MyBindings"
= { = "sumValues", = "getValue" }
= ["internal_helper"]
= { = "./other_bindings.js" }
See docs/configuration.md for the full reference.
Naming: separate UniFFI crates
If your UniFFI crate is a thin wrapper around a library (e.g., html2markdown-uniffi wrapping html2markdown), the auto-derived namespace will include the suffix — Html2markdownUniffi. Use module_name to choose a clean name:
# uniffi.toml
[]
= "Html2Markdown"
// Before: Html2markdownUniffi.convert(html)
// After: Html2Markdown.convert(html)
This is especially common when you keep UniFFI scaffolding in a separate crate to avoid feature-flag conflicts or to support multiple binding targets.
External types
External types declared with [External="crate_name"] in UDL require a corresponding entry in external_packages:
[]
= { = "./other_bindings.js" }
The generator emits named imports from the configured path.
Features
- All UniFFI primitives, strings, bytes, timestamps (
Date), and durations - Records as TypeScript
interfacetypes with optional field defaults - Flat enums (string literal unions) and data-carrying enums (discriminated unions)
- Objects with constructors, methods,
free()lifecycle, andSymbol.dispose - Flat and rich error classes via
[Error]and[Throws] - Async functions and methods mapped to
Promise<T> - Callback interfaces with VTable FFI glue
- Trait interfaces with object return lifting
- Custom type aliases and external type imports
- Rename, exclude, and docstring (JSDoc) support
- Enum methods, constructors, and discriminant annotations
- Non-exhaustive enums and errors with catch-all variants
- Default argument values and optional parameters
Platform Requirements
Generated bindings require:
- ES2022 modules — top-level
awaitis used to load the WASM module. FinalizationRegistry— used as a safety net for preventing leaked object handles (supported in all modern engines; a no-op polyfill is included for older environments).WebAssembly.Function(Type Reflection proposal) — required only when using callback interfaces or async functions. These features need typed WASM trampolines via__indirect_function_table. Supported in V8 (Chrome, Node.js 22+) and SpiderMonkey (Firefox). Safari 18.2+ added support; older Safari versions may not work.
Rust crate setup
The Rust crate must enable two UniFFI feature flags:
= { = "0.31", = ["scaffolding-ffi-buffer-fns", "wasm-unstable-single-threaded"] }
scaffolding-ffi-buffer-fnsgenerates an alternate FFI layer where every function uses a uniform(argPtr, retPtr)calling convention instead of per-function signatures. This is what the generated TypeScript calls into. Also used by Mozilla's gecko-js bindings.wasm-unstable-single-threadedopts out ofSend + Syncrequirements on UniFFI objects when targetingwasm32, since WASM is single-threaded. The "unstable" label reflects the evolving state of WASM threading support; the feature itself has been stable since uniffi 0.27.
For callback interfaces and async, also set:
RUSTFLAGS="-C link-arg=--export-table -C link-arg=--growable-table"
Compatibility
| uniffi-bindgen-js | uniffi-rs |
|---|---|
| 0.1.x | 0.31.0 |
Contributing
See CONTRIBUTING.md for guidelines.
License
MIT