tsify

repository·main·Indexed 19 days ago

https://github.com/madonoharu/tsify

A Rust library for generating TypeScript definitions from Rust code, specifically designed for WebAssembly projects using wasm-bindgen. It provides the Tsify trait and Ts<T> wrapper to facilitate seamless conversion between Rust types and JavaScript values, supporting custom type overrides, optional properties, and enum-to-union type conversion.

Tokens
4.3K
Snippets
19
Records
21
Agent score
66%

What's inside tsify

  1. Use Tsify with wasm-bindgen to generate TypeScript definitions

    main

    Tsify integrates with wasm-bindgen to automatically output .d.ts files. Instead of using deprecated attributes like into_wasm_abi, use the Ts<T> wrapper for function parameters and return types. This allows you to convert between Rust and JS using .into_ts() and .to_rust().

    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()?.into())
    }
    
    #[wasm_bindgen]
    pub fn from_js(point: Ts<Point>) -> Result<(), JsError> {
        let point: Point = point.to_rust()?;
        Ok(())
    }
  2. Verify `.d.ts` output using e2e tests

    main

    The tests-e2e suite is used to validate that the .d.ts files generated by wasm-pack match the expected reference outputs.

    To perform an end-to-end verification:

    1. Navigate to one of the sub-folders (e.g., test1, test2, etc.).
    2. Run wasm-pack build to generate the pkg/ directory containing the TypeScript definitions.
    3. Run the comparison script from the repository root to compare the generated pkg/ output against the stored reference output.
    # 1. Build the project in a sub-folder
    wasm-pack build
    
    # 2. Compare the generated pkg/ output with the reference
    ./reference_output/compare_output.sh
  3. Install Tsify

    main

    To use Tsify, add it to your Cargo.toml along with serde (with derive feature enabled) and wasm-bindgen.

    [dependencies]
    tsify = "0.5.5"
    serde = { version = "1.0", features = ["derive"] }
    wasm-bindgen = { version = "0.2" }
  4. Use the Ts<T> wrapper for robust WASM interop

    main

    When passing types between Rust and JavaScript using #[wasm_bindgen], use the Ts<T> wrapper instead of direct types. Ts<T> acts as a robust wrapper around a JsValue that prevents memory leaks during (de)serialization failures, which can occur when using #[tsify(into_wasm_abi, from_wasm_abi)] attributes.

    It is highly recommended to use Ts<T> in conjunction with the Result<_, JsError> pattern. This allows you to catch deserialization or serialization errors and throw them as JavaScript errors rather than panicking the WASM runtime.

    Key methods:

    • to_rust(): Converts the inner JavaScript value into the Rust type T. Returns Result<T, Error>.
    • from_rust(rust: &T): Converts a Rust type T into a Ts<T> wrapper. Returns Result<Self, Error>.
    • into_ts(): (Available via the Tsify trait) A convenient way to convert a Rust type into a Ts<T> wrapper.
    • js_value(): Returns the underlying JsValue (zero-cost).
    • new_unchecked(js: JsValue): Reinterprets a JsValue as Ts<T> without validation (zero-cost).
    use tsify::Tsify;
    use tsify::Ts;
    use wasm_bindgen::prelude::*;
    use wasm_bindgen::JsError;
    
    #[derive(Tsify, serde::Deserialize, serde::Serialize)]
    pub struct Vec2 {
       x: f64,
       y: f64,
    }
    
    #[wasm_bindgen]
    pub fn rotate(v: Ts<Vec2>, theta_rad: f64) -> Result<Ts<Vec2>, JsError> {
        // Deserialize to rust type, throw deserialization error if fails
        let Vec2 { x, y } = v.to_rust().map_err(|e| JsError::new(&format!("{:?}", e)))?;
        
        let cos = theta_rad.cos();
        let sin = theta_rad.sin();
        let result = Vec2 {
            x: x * cos - y * sin,
            y: x * sin + y * cos,
        };
        
        // Serialize back to JsValue, throw serialization error if fails
        Ok(result.into_ts().map_err(|e| JsError::new(&format!("{:?}", e)))?)
    }
  5. Configure Tsify crate features

    main

    Tsify provides two main features for serialization:

    • json (default): Enables serialization through serde_json.
    • js: Enables serialization through serde-wasm-bindgen and generates the appropriate types for it. (Note: This is intended to be the default in future versions).
  6. Generate a TypeScript namespace for Enums

    main

    By adding #[tsify(namespace)] to an enum, Tsify generates both a namespace containing exported types for each variant and a union type for the enum itself.

    use tsify::Tsify;
    
    #[derive(Tsify)]
    #[tsify(namespace)]
    enum Color {
        Red,
        Blue,
        Green,
        Rgb(u8, u8, u8),
        Hsv {
            hue: f64,
            saturation: f64,
            value: f64,
        },
    }
  7. Handle optional properties in TypeScript

    main

    To make a field optional in the generated TypeScript interface, use the #[tsify(optional)] attribute. You can also use standard serde attributes like skip_serializing_if to control how Option types are handled during serialization.

    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,
    }
  8. Declare TypeScript type aliases with #[declare]

    main

    Use the #[declare] attribute to create a top-level TypeScript type alias from a Rust type.

    use tsify::{declare, Tsify};
    
    #[derive(Tsify)]
    struct Foo<T>(T);
    
    #[declare]
    type Bar = Foo<i32>;
  9. Generate TypeScript types for Rust Enums

    main

    Tsify converts Rust enums into TypeScript union types. Simple unit variants become string literals, while tuple or struct variants become object types containing the variant name.

    use tsify::Tsify;
    
    #[derive(Tsify)]
    enum Color {
        Red,
        Blue,
        Green,
        Rgb(u8, u8, u8),
        Hsv {
            hue: f64,
            saturation: f64,
            value: f64,
        },
    }
  10. Override TypeScript types with #[tsify(type = ...)]

    main

    You can manually specify the TypeScript type for a field or a container using the type attribute.

    use tsify::Tsify;
    
    #[derive(Tsify)]
    pub struct Foo {
        #[tsify(type = "0 | 1 | 2")]
        x: i32,
    }
  11. Configure serialization behavior with SerializationConfig

    main

    The SerializationConfig struct controls how Rust types are mapped to JavaScript objects during the into_js() process. This is particularly relevant when using the js feature with serde_wasm_bindgen.

    Available configuration fields:

    • missing_as_null: If true, missing fields in the Rust struct will be serialized as null in JavaScript.
    • hashmap_as_object: If true, Rust HashMaps will be serialized as plain JavaScript objects instead of Map-like structures.
    • large_number_types_as_bigints: If true, large numbers will be serialized as JavaScript BigInts to prevent precision loss.
    pub struct SerializationConfig {
        pub missing_as_null: bool,
        pub hashmap_as_object: bool,
        pub large_number_types_as_bigints: bool,
    }
    
    // The default configuration used by the trait is:
    // missing_as_null: false
    // hashmap_as_object: false
    // large_number_types_as_bigints: false
  12. Convert Rust type to Ts<T> with from_rust()

    main

    If you have a Rust type T that implements Tsify and serde::Serialize, you can wrap it in a Ts<T> for return to JavaScript using Ts::from_rust(rust: &T). This performs the serialization into a JavaScript-compatible type.

    Returns: Result<Ts<T>, Error>.

    // Assuming T: Tsify + serde::Serialize
    let ts_wrapper = Ts::from_rust(&my_rust_struct)?;