ts-function 0.2.0

A proc-macro that generates TypeScript type aliases and wasm-bindgen ABI trait impls for Rust callback wrapper structs
Documentation
# ts-function

[![Rust 2024](https://img.shields.io/badge/edition-2024-red.svg)](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.

| 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]#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.