Skip to content

logankaser/ts-function

Repository files navigation

ts-function

Rust 2024

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 wasm_bindgen::prelude::*;
use ts_function::ts_function;
use ts_macro::ts;

// 1. Define your callback signatures in pure Rust using type aliases
#[ts_function]
pub type SingleArgCb = fn(msg: String);

#[ts_function]
pub type MultiArgCb = fn(a: f64, b: js_sys::Uint8Array);

// 2. Use `ts_macro` to create a struct containing your callbacks
#[ts]
struct AppCallbacks {
    on_ready: SingleArgCb,
    on_data: MultiArgCb,
}

// 3. Export a function that accepts your callbacks from JS
#[wasm_bindgen]
pub fn execute_callbacks(cbs: IAppCallbacks) {
    // `.parse()` safely extracts the `js_sys::Function` objects into your strongly-typed wrappers
    let callbacks: AppCallbacks = cbs.parse();
    
    // Call the functions! Arguments are automatically converted to `JsValue`.
    // The `call` method returns a `Result<(), JsValue>` forwarding any JS exceptions.
    callbacks.on_ready.call("System is ready".to_string()).unwrap();
    
    let arr = js_sys::Uint8Array::new_with_length(3);
    arr.copy_from(&[1, 2, 3]);
    callbacks.on_data.call(42.5, arr).unwrap();
}

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

#[ts_function]
pub type CalculateCb = fn(a: f64) -> f64;

// Usage:
let result: f64 = calculate_cb.call(10.0).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:

#[ts_function]
pub type LargeDataCb = fn() -> js_sys::Uint8Array;

// Usage (Zero-Copy):
let arr: js_sys::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 wasm_bindgen::prelude::*;
use ts_function::ts_function;

// 1. Define your own tuple struct
pub struct CustomLoggingCallback(pub js_sys::Function);

// 2. Apply the macro to the impl block
#[ts_function]
impl CustomLoggingCallback {
    // 3. Define the `call` signature exactly how you want it exposed to TypeScript
    pub fn call(&self, val: f64) {
        // You are responsible for all conversions and calling the JS function
        let result = self.0.call1(
            &wasm_bindgen::JsValue::NULL,
            &wasm_bindgen::JsValue::from_f64(val),
        );

        // Handle errors internally however you want
        if let Err(e) = result {
            if let Ok(err_obj) = e.dyn_into::<js_sys::Error>() {
                web_sys::console::error_1(&err_obj.message().into());
            }
        }
    }
}

In the example above, ts-function will emit export type CustomLoggingCallback = (val: number) => void;.

License

Dual-licensed under either the MIT license or the Apache License, Version 2.0 at your option.

About

Typed JS functions support for wasm-bindgen / ts_macro

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors