Tsify is a library for generating TypeScript definitions from Rust code.
Using this with wasm-bindgen will automatically output the types to .d.ts.
Inspired by typescript-definitions and ts-rs.
Click to show Cargo.toml.
[dependencies]
tsify = "0.5.7"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "0.2" }use serde::{Deserialize, Serialize};
use tsify::Tsify;
use tsify::Ts;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsError;
#[derive(Tsify, Serialize, Deserialize)]
pub struct Point {
x: i32,
y: i32,
}
#[wasm_bindgen]
pub fn into_js() -> Result<Ts<Point>, JsError> {
let point = Point { x: 0, y: 0 };
Ok(point.into_ts()?)
}
#[wasm_bindgen]
pub fn from_js(point: Ts<Point>) -> Result<(), JsError> {
let point: Point = point.to_rust()?;
Ok(())
}Will generate the following .d.ts file:
/* tslint:disable */
/* eslint-disable */
export interface Point {
x: number;
y: number;
}
export function from_js(point: Point): void;
export function into_js(): Point;This is the behavior due to typescript_custom_section and Rust Type conversions.
json(default) enables serialization throughserde_json.jsenables serialization throughserde-wasm-bindgenand generates the appropriate types for it. This will be the default in future versions.
Tsify container attributes
into_wasm_abi(deprecated) implementsIntoWasmAbiandOptionIntoWasmAbi. This can be converted directly from Rust to JS viaserde_jsonorserde-wasm-bindgen. Deprecated in favour of usingTs<T>as on function parameters and return type (why).from_wasm_abi(deprecated) implementsFromWasmAbiandOptionFromWasmAbi. This is the opposite operation of the above. Deprecated in favour of usingTs<T>as on function parameters and return type (why).namespacegenerates a namespace for the enum variants.typeoverrides at the container level.type_paramsoverrides params at the container level.
Serializer configuration options
missing_as_nullhashmap_as_objectlarge_number_types_as_bigints
Tsify field attributes
typetype_paramsoptional
Serde attributes
renamerename-alltagcontentuntaggedskipskip_serializingskip_deserializingskip_serializing_if = "Option::is_none"flattendefaulttransparent
#[tsify(into_wasm_abi, from_wasm_abi)] moves (de)serialization into the wasm-bindgen ABI boundary, and that boundary cannot report failure.
wasm_bindgen::convert::FromWasmAbi::from_abi returns Self, not Result<Self, _>, and there is no fallible variant of it or of RefFromWasmAbi / LongRefFromWasmAbi / VectorFromWasmAbi. So when serde fails to deserialize what JavaScript passed in, the generated impl has only one way out: wasm_bindgen::throw_str, which raises a JavaScript exception that unwinds straight past the wasm frames. As wasm-bindgen's own documentation warns:
Note that it is very easy to leak memory with this function because this function, unlike
panic!on other platforms, will not run destructors.
Everything alive at that moment leaks: the serde error, the partially deserialized value, and — because arguments are converted one after another — every argument already converted before the failing one. From JavaScript this looks like an ordinary, catchable exception, so an application can appear to handle bad input correctly while its wasm heap grows on every failure, until the instance dies with RuntimeError: memory access out of bounds (see #65 and #86).
Ts<T> keeps the boundary infallible: it is a #[repr(transparent)] wrapper whose FromWasmAbi impl only forwards the underlying JsValue. Deserialization then happens inside your function, where it is an ordinary Result — the from_js example at the top of this page shows the shape. Because the function returns normally, destructors run and nothing leaks. The generated TypeScript is unchanged, so .d.ts consumers are unaffected.
Ts<T> needs only #[derive(Tsify)] — do not add #[tsify(from_wasm_abi)] alongside it. Note also that Ts<Vec<T>> is not supported, only Vec<Ts<T>>; to convert a whole vector, use items.into_iter().map(|x| x.to_rust()).collect::<Result<Vec<_>, _>>()?.
use tsify::Tsify;
#[derive(Tsify)]
pub struct Foo {
#[tsify(type = "0 | 1 | 2")]
x: i32,
}Generated type:
export interface Foo {
x: 0 | 1 | 2;
}use tsify::Tsify;
#[derive(Tsify)]
struct Optional {
#[tsify(optional)]
a: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
b: Option<String>,
#[serde(default)]
c: i32,
}Generated type:
export interface Optional {
a?: number;
b?: string;
c?: number;
}use tsify::Tsify;
#[derive(Tsify)]
enum Color {
Red,
Blue,
Green,
Rgb(u8, u8, u8),
Hsv {
hue: f64,
saturation: f64,
value: f64,
},
}Generated type:
export type Color =
| "Red"
| "Blue"
| "Green"
| { Rgb: [number, number, number] }
| { Hsv: { hue: number; saturation: number; value: number } };use tsify::Tsify;
#[derive(Tsify)]
#[tsify(namespace)]
enum Color {
Red,
Blue,
Green,
Rgb(u8, u8, u8),
Hsv {
hue: f64,
saturation: f64,
value: f64,
},
}Generated type:
declare namespace Color {
export type Red = "Red";
export type Blue = "Blue";
export type Green = "Green";
export type Rgb = { Rgb: [number, number, number] };
export type Hsv = {
Hsv: { hue: number; saturation: number; value: number };
};
}
export type Color = Color.Red | Color.Blue | Color.Green | Color.Rgb | Color.Hsv;use tsify::{declare, Tsify};
#[derive(Tsify)]
struct Foo<T>(T);
#[declare]
type Bar = Foo<i32>;Generated type:
export type Foo<T> = T;
export type Bar = Foo<number>;