ts-function
A proc-macro that generates TypeScript type aliases and wasm-bindgen ABI trait
implementations for Rust callback wrapper structs, enabling strongly-typed
Typescript Functions types in ts-macro projects with little to no boilerplate.
Motivation
When using ts-macro to generate
TypeScript interfaces for Rust structs, there is no built-in support for
function or callback types. A struct field like on_click: js_sys::Function
produces an opaque Function type in TypeScript and forces you to manually
handle raw JsValue conversions in Rust.
ts-function bridges this gap. It allows you to define callback signatures in
pure Rust, automatically generating the correct TypeScript types ((args: ...) => void) and implementing the wasm-bindgen ABI traits required to pass those
callbacks across the Wasm boundary safely.
Example: Callback Struct Pattern
This pattern which in my opinion, is a good way to hook a Rust WASM App like into a Typescript codebase, Is the modivating use case for this project. Specifically, I wanted to do this for a Bevy project of mine, embeded inside a React frontend. Something I found to work well was a pattern where rust code called callbacks which modify valtio state, causing React to re-render. (Also, for the record I would 100% have chosen something more "interesting" than React, but this is a work project)
Rust (src/lib.rs)
use *;
use ;
// 1. Define your callback signatures in pure Rust using type aliases
pub type SingleArgCb = fn;
pub type MultiArgCb = fn;
// 2. Use `ts_macro` to create a struct containing your callbacks
// 3. Export a function that accepts your callbacks from JS
Generated TypeScript (index.d.ts)
wasm-bindgen outputs the types you would expect,
using the wonderful ts-type crate,
meaning zero #ts[(type = "...")] manual overrides are required.
/* tslint:disable */
/* eslint-disable */
// The type aliases generated by `#[ts_function]`
type SingleArgCb = (msg: string) => void;
type MultiArgCb = (a: number, b: Uint8Array) => void;
// The interface generated by `#[ts]` in `ts-macro`
interface IAppCallbacks {
onReady: SingleArgCb;
onData: MultiArgCb;
}
export function execute_callbacks(cbs: IAppCallbacks): void;
Supported Types
ts-function supports a wide range of Rust types, automatically mapping them to their idiomatic TypeScript equivalents.
| Rust Type | TypeScript Type | Notes |
|---|---|---|
f32, f64, i8-i32, u8-u32 |
number |
|
i64, u64 |
bigint |
Mapped via js_sys::BigInt |
bool |
boolean |
|
String, &str |
string |
|
Option<T> |
T | undefined |
|
Vec<u8>, &[u8] |
Uint8Array |
See Zero-Copy Performance Tip |
Vec<f64>, &[f64] |
Float64Array |
Also supports i8, u16, i32, etc. |
JsValue |
any |
|
js_sys::Object |
object |
|
IMyInterface |
IMyInterface |
Any JsCast type (including ts-macro interfaces) |
Return Values & Performance
The #[ts_function] macro supports returning values from JavaScript back into
Rust. It handles standard primitives, strings, options, and even numeric vectors
(via TypedArray).
Example: Returning a Value
pub type CalculateCb = fn ;
// Usage:
let result: f64 = calculate_cb.call.unwrap;
Zero-Copy Performance Tip
When returning large arrays, using Vec<u8> or similar will force a memory copy
from the JavaScript heap to the Rust heap. To achieve zero-copy performance,
you can return a js_sys type directly:
pub type LargeDataCb = fn ;
// Usage (Zero-Copy):
let arr: Uint8Array = large_data_cb.call.unwrap;
The macro uses JsCast::unchecked_into for these types, allowing you to work
directly with the JavaScript memory view.
Type Safety & Runtime Overhead
By relying on the generated TypeScript signatures, ts-function avoids some of
the runtime overhead typically associated with JsValue dynamic type checking.
Because the TypeScript compiler ensures that the JavaScript environment satisfies
the function's contract, we can use unchecked casts in the generated Rust glue
code.
Advanced Usage: The Escape Hatch
Now that ts-function natively supports returning values and returning Result<T, JsValue> for errors,
the primary API via type aliases (pub type MyCb = fn(...) -> ...) handles almost all use cases.
However, if you need completely custom serialization, have complex types not supported natively by the macro,
or want to embed specific side-effects and error handling directly into the callback execution, you can use the escape hatch.
By applying #[ts_function] to an impl block for a tuple-struct wrapping js_sys::Function, you take complete control of the call method's implementation.
ts-function will still parse your call method's signature and automatically generate the correct TypeScript interface for it!
use *;
use ts_function;
// 1. Define your own tuple struct
;
// 2. Apply the macro to the impl block
In the example above, ts-function will emit export type CustomLoggingCallback = (val: number) => void;.
In-Sourced Macros
This crate currently provides an updated version of the #[ts] macro (originally from ts-macro), as well as the underlying ts-type logic. These have been brought in-house to support the advanced type generation required for ts-function pending the acceptance of PRs into the upstream project.
When those PRs are accepted, this crate will revert to depending on the official releases.
License
Dual-licensed under either the MIT license or the Apache License, Version 2.0 at your option.