# ts-function
[](https://rust-lang.org)
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`](https://crates.io/crates/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](https://bevy.org/) 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](https://valtio.dev/) 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`)
```rust
use wasm_bindgen::prelude::*;
use ts_function::{ts, ts_function};
// 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](https://crates.io/crates/ts-type) crate,
meaning zero `#ts[(type = "...")]` manual overrides are required.
```typescript
/* 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.
| `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](#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
```rust
#[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:
```rust
#[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!**
```rust
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;`.
## 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](https://github.com/ryangoree/wasm-utils-rs).
When those PRs are accepted, this crate will revert to depending on the official releases.
## License
Dual-licensed under either the [MIT license](LICENSE-MIT) or the [Apache License, Version 2.0](LICENSE-APACHE) at your option.