Code Monkey home page Code Monkey logo

tsify's Issues

Tsify without wasm_bindgen

Hello, I was just wondering if it's possible to decouple this from wasm_bindgen.

I understand that the original purpose of the library was to focus on wasm_bindgen but I was wondering if this could become a more generalised library for creating cross-language type definitions, similar to https://github.com/1Password/typeshare

Customizing struct name / namespace?

I have two files that each define a struct with the same "basename", e.g.:

// a.rs
#[derive(Tsify)]
pub struct Foo {
    pub n: usize,
}

// b.rs
#[derive(Tsify)]
pub struct Foo {
    pub s: String,
}

They end up as duplicate, top-level Foo declarations in the exported .d.ts file:

// my_package.d.ts
export interface Foo {
    n: number;
}
export interface Foo {
    s: string;
}

Is there a way to namespace, or otherwise rename them?

Rustdoc comments are not included in TS output

Hi! I want to use tsify to generate types (duh :D) for a public API, and I would like my rustdoc comments on my structs and fields to transport over to the TS definitions.

E.g. I have

/// JSON-compatible and human readable format of transactions. E.g. addresses are presented in their human-readable
/// format and address types and the network are represented as strings. Data and proof are serialized as an object
/// describing their contents.
#[derive(serde::Serialize, serde::Deserialize, Tsify)]
#[serde(rename_all = "camelCase")]
pub struct PlainTransaction {
    /// The transaction's unique hash, used as its identifier. Sometimes also called `txId`.
    pub transaction_hash: String,

    // [...]
}

but unfortunately the generated TS types don't include those doc comments:

export interface PlainTransaction {
    transactionHash: string;
}

I would like the output to be this instead:

/**
 * JSON-compatible and human readable format of transactions. E.g. addresses are presented in their human-readable
 * format and address types and the network are represented as strings. Data and proof are serialized as an object
 * describing their contents.
 */
export interface PlainTransaction {
    /**
     * The transaction's unique hash, used as its identifier. Sometimes also called `txId`.
     */
    transactionHash: string;
}

wasm-bindgen does this, it converts the rustdoc into JSDoc annotations.

Is this complicated?

Internally tagged enums can generate invalid TS when using non-object types

If you have an enum such as this:

#[derive(Debug, Serialize, Deserialize, Tsify)]
#[serde(tag = "reason")]
pub enum ParseBaseUriError {
    MissingTrailingSlash,
    UrlParseError(String),
    CannotBeABase,
}

then the generated types look as follows:

declare namespace ParseBaseUriError {
    export type MissingTrailingSlash = { reason: "MissingTrailingSlash" };
    export type UrlParseError = { reason: "UrlParseError" } & string;
    export type CannotBeABase = { reason: "CannotBeABase" };
}

And I believe UrlParseError is unsatisfiable.

I don't really have a good suggestion for how to resolve this, perhaps disallowing new-type like variants, or possibly only ones that contain types that can be mapped to a non-object JS type. For now I have resolved this by picking a different tagging mechanism, or turning the variants into structs, but I thought I'd flag in case there are any other ideas or to allow adding a warning.

Entire Wasm instance is invalidated if serde impl from macro crashes on deserialization step

Here is a minimal example.

Make a new project. Use these files:

lib.rs:

use std::sync::Arc;

use js_sys::Promise;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;

extern crate web_sys;

// A macro to provide `println!(..)`-style syntax for `console.log` logging.
#[macro_export]
macro_rules! console_log {
    ( $( $t:tt )* ) => {
        web_sys::console::log_1(&format!( $( $t )* ).into());
    }
}

#[wasm_bindgen]
pub struct MyApi {
    pub(crate) api: Arc<tokio::sync::RwLock<ApiImpl>>,
}

#[wasm_bindgen]
pub struct ApiImpl {
    pub(crate) name: String,
    pub(crate) some_stuff: Arc<tokio::sync::RwLock<u32>>,
}

#[wasm_bindgen]
impl ApiImpl {
    pub fn new(name: String) -> Self {
        Self {
            name,
            some_stuff: Arc::new(RwLock::new(0)),
        }
    }

    pub fn get_name(&self) -> String {
        self.name.clone()
    }

    pub async fn do_stuff(&self) -> u32 {
        let mut some_stuff = self.some_stuff.write().await;
        *some_stuff += 1;
        *some_stuff
    }

    pub async fn get_stuff(&self) -> u32 {
        let some_stuff = self.some_stuff.read().await;
        *some_stuff
    }

    pub fn add_game_to_cart(
        &mut self,
        game_item_dbid: Gid,
        // game_group_dbid: Option<Gid>,
        // source: PlayerActionSource,
    ) -> Result<String, String> {
        if game_item_dbid.data == 0 {
            Err("game_group_dbid is none".to_string())
        } else {
            Ok("ok".to_string())
        }
    }
}

#[wasm_bindgen]
impl MyApi {
    pub fn new(name: String) -> Self {
        Self {
            api: Arc::new(RwLock::new(ApiImpl::new(name))),
        }
    }

    pub async fn get_name(&self) -> String {
        let api = self.api.read().await;
        api.get_name()
    }

    pub async fn do_stuff(&self) -> u32 {
        let mut api = self.api.write().await;
        api.do_stuff().await
    }

    pub async fn get_stuff(&self) -> u32 {
        let api = self.api.read().await;
        console_log!("get_stuff");
        api.get_stuff().await
    }

    pub fn add_game_to_cart(
        &mut self,
        game_item_dbid: Gid,
        // game_group_dbid: Option<Gid>,
        // source: PlayerActionSource,
    ) -> Promise {
        console_log!("add_game_to_cart");
        let api = self.api.clone();
        console_log!("add_game_to_cart");
        future_to_promise(async move {
            let mut api = api.write().await;
            let result = api
                .add_game_to_cart(game_item_dbid) //, game_group_dbid, source)
                .to_js_result()?;
            let serializer = serde_wasm_bindgen::Serializer::json_compatible();
            let result = result.serialize(&serializer);
            Ok(result?)
        })
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify::Tsify)]
#[tsify(from_wasm_abi)]
#[serde(tag = "kind", content = "payload")]
#[serde(rename_all = "snake_case")]
pub enum PlayerActionSource {
    Unknown,
    Aig,
    Navigation,
    Search { search_string: String },
}

#[derive(
    Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Clone, serde::Serialize, serde::Deserialize,
)]
#[cfg_attr(
    any(target_arch = "wasm32", target_os = "macos"),
    derive(tsify::Tsify),
    tsify(from_wasm_abi, into_wasm_abi)
)]
pub struct Gid {
    pub data: u128,
}

use wasm_bindgen::JsValue;

pub type JsResult<T> = Result<T, JSError>;

pub trait ToJSResultTrait<T>: Sized {
    fn to_js_result(self) -> JsResult<T>;

    fn to_js_result_msg(self, msg: &str) -> JsResult<T> {
        match self.to_js_result() {
            Ok(value) => Ok(value),
            Err(err) => Err(JSError {
                message: format!("{}: {}", msg, err.message),
            }),
        }
    }
}

impl<T> ToJSResultTrait<T> for Option<T> {
    fn to_js_result(self) -> JsResult<T> {
        match self {
            Some(value) => Ok(value),
            None => Err(JSError {
                message: "Option is None".to_string(),
            }),
        }
    }
}

impl<T> ToJSResultTrait<T> for Result<T, String> {
    fn to_js_result(self) -> JsResult<T> {
        match self {
            Ok(value) => Ok(value),
            Err(err) => Err(JSError { message: err }),
        }
    }
}

impl<T> ToJSResultTrait<T> for Result<T, reqwest::Error> {
    fn to_js_result(self) -> JsResult<T> {
        match self {
            Ok(value) => Ok(value),
            Err(err) => Err(JSError {
                message: err.to_string(),
            }),
        }
    }
}

impl From<JSError> for JsValue {
    fn from(error: JSError) -> JsValue {
        serde_wasm_bindgen::to_value(&error).unwrap()
    }
}

use wasm_bindgen::prelude::wasm_bindgen;

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify::Tsify)]
#[tsify(from_wasm_abi)]
pub struct JSError {
    pub message: String,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify::Tsify)]
#[tsify(from_wasm_abi)]
pub struct JSAnalyticItem {
    pub order_id: String,
    pub action_type: String,
    pub test_id: Option<i16>,
    pub object: Option<String>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify::Tsify)]
#[tsify(from_wasm_abi)]
#[serde(tag = "variant", content = "data")]
#[serde(rename_all = "snake_case")]
pub enum JSStreamStatus {
    Start,
    Retry(i8),
    GiveUp,
    Error(String),
}

Cargo.toml

[workspace]
resolver = "2"

[package]
name = "learn-rust-wasm"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
async-trait = "0.1.77"
cfg-if = "1.0.0"
futures = "0.3.30"
getrandom = {version = "0.2.8", features = ["js"] }
rand = "0.8.5"
reqwest = "0.11.23"
serde = "1.0.195"
serde-wasm-bindgen = "0.6.3"
serde_json = "1.0.111"
serde_yaml = "0.9.30"
tokio = { version = "1.35.1", features = ["macros", "rt", "sync"] }
tokio-stream = "0.1.14"
tracing = "0.1.40"
tsify = "0.4.5"
wasm-bindgen = "0.2.90"
wasm-bindgen-futures = "0.4.40"
console_error_panic_hook = { version = "0.1.6", optional = true }
wee_alloc = { version = "0.4.5", optional = true }
js-sys = "0.3.67"
tracing-wasm = "0.2.1"
wasm-streams = "0.4.0"
ws_stream_wasm = "0.7.4"
web-sys = {version = "0.3", features = [ "console", "ReadableStream", "BinaryType", "Blob", "ErrorEvent", "FileReader", "MessageEvent", "ProgressEvent", "WebSocket", ]}

.cargo/config.toml

[build]
target = "wasm32-unknown-unknown"
rustflags = ["--cfg", "tokio_unstable"]
rustdocflags = ["--cfg", "tokio_unstable"]

web/index.js

import { MyApi } from "learn_rust_wasm";

export async function debug_all() {

    // console.log('----------------- debug_wasm() -----------------');
    // await debug_wasm();
}

export function getApi() {
    let api = MyApi.new("lol");
    debugger;
    return api;
}

let x = getApi();

window.api = x;
console.log(await window.api.get_name());
console.log(await window.api.do_stuff());

web/bootstrap.js

// A dependency graph that contains any wasm must all be imported
// asynchronously. This `bootstrap.js` file does the single async import, so
// that no one else needs to worry about it again.
import("./index.js")
  .catch(e => console.error("Error importing `index.js`:", e));

let api = import("learn_rust_wasm");

web/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello wasm-pack!</title>
  </head>
  <body>
    <noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
    <script src="./bootstrap.js"></script>
  </body>
</html>

web/package.json

{
  "name": "create-wasm-app",
  "version": "0.1.0",
  "description": "create an app to consume rust-generated wasm packages",
  "main": "index.js",
  "bin": {
    "create-wasm-app": ".bin/create-wasm-app.js"
  },
  "scripts": {
    "build": "webpack --config webpack.config.js",
    "start": "webpack-dev-server --mode development"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/rustwasm/create-wasm-app.git"
  },
  "keywords": [
    "webassembly",
    "wasm",
    "rust",
    "webpack"
  ],
  "author": "Ashley Williams <[email protected]>",
  "license": "(MIT OR Apache-2.0)",
  "bugs": {
    "url": "https://github.com/rustwasm/create-wasm-app/issues"
  },
  "homepage": "https://github.com/rustwasm/create-wasm-app#readme",
  "devDependencies": {
    "copy-webpack-plugin": "^5.0.0",
    "hello-wasm-pack": "^0.1.0",
    "webpack": "^5.89.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1"
  },
  "dependencies": {
    "learn_rust_wasm": "file:../pkg"
  }
}

web/webpack.config.js

const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require('path');

module.exports = {
  entry: "./bootstrap.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bootstrap.js",
  },
  mode: "development",
  plugins: [
    new CopyWebpackPlugin(['index.html'])
  ],
  experiments: {
    asyncWebAssembly: true,
  },
  devtool: 'source-map'
};

then run:

wasm-pack build; (cd web && npm i && npm run start)

When you go to http://localhost:8080 and then run this, it will fail in this way:

>>> window.api.add_game_to_cart({})
Uncaught Error: missing field `data` at line 1 column 2
    __wbindgen_throw learn_rust_wasm_bg.js:537
    add_game_to_cart learn_rust_wasm_bg.js:384
    <anonymous> debugger eval code:1
[learn_rust_wasm_bg.js:537](webpack://create-wasm-app/pkg/learn_rust_wasm_bg.js)
    __wbindgen_throw learn_rust_wasm_bg.js:537
    <anonymous> 3456e6f9d69608745039.module.wasm:91904
    <anonymous> 3456e6f9d69608745039.module.wasm:65348
    add_game_to_cart learn_rust_wasm_bg.js:384
    <anonymous> debugger eval code:1
>>> window.api.add_game_to_cart({})
Uncaught Error: recursive use of an object detected which would lead to unsafe aliasing in rust
    __wbindgen_throw learn_rust_wasm_bg.js:537
    add_game_to_cart learn_rust_wasm_bg.js:384
    <anonymous> debugger eval code:1
[learn_rust_wasm_bg.js:537](webpack://create-wasm-app/pkg/learn_rust_wasm_bg.js)
>>> window.api.get_stuff({})
Uncaught Error: recursive use of an object detected which would lead to unsafe aliasing in rust
    __wbindgen_throw learn_rust_wasm_bg.js:537
    __wbg_adapter_22 learn_rust_wasm_bg.js:216
    real learn_rust_wasm_bg.js:201
    __wbg_queueMicrotask_118eeb525d584d9a learn_rust_wasm_bg.js:437
    __wbg_adapter_49 learn_rust_wasm_bg.js:227
    cb0 learn_rust_wasm_bg.js:501
    __wbg_new_1d93771b84541aa5 learn_rust_wasm_bg.js:506
    get_stuff learn_rust_wasm_bg.js:376
    <anonymous> debugger eval code:1
[learn_rust_wasm_bg.js:537](webpack://create-wasm-app/pkg/learn_rust_wasm_bg.js)

Trait `From<...>` is not implemented for `JsValue` when returning structs in async functions

Click to show Cargo.toml.
[package]
name = "asynctest"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
tsify = "0.4.5"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "0.2" }
wasm-bindgen-futures = "0.4"

[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"

Using the following code, as in the example in the README, but when using an async function to return a struct:

use serde::{Deserialize, Serialize};
use tsify::Tsify;
use wasm_bindgen::prelude::*;

#[derive(Tsify, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Point {
    x: i32,
    y: i32,
}

#[wasm_bindgen]
pub async fn into_js() -> Point {
    Point { x: 0, y: 0 }
}

#[wasm_bindgen]
pub fn from_js(point: Point) {}

The compiler complains when executing wasm-pack build --target web --release:

[INFO]: Checking for the Wasm target...
[INFO]: Compiling to Wasm...
   Compiling asynctest v0.1.0
error[E0277]: the trait bound `JsValue: From<Point>` is not satisfied
  --> src/lib.rs:12:1
   |
12 | #[wasm_bindgen]
   | ^^^^^^^^^^^^^^^ the trait `From<Point>` is not implemented for `JsValue`
   |
   = help: the following other types implement trait `From<T>`:
             <JsValue as From<&'a T>>
             <JsValue as From<&'a std::string::String>>
             <JsValue as From<&'a str>>
             <JsValue as From<*const T>>
             <JsValue as From<*mut T>>
             <JsValue as From<JsError>>
             <JsValue as From<JsType>>
             <JsValue as From<bool>>
           and 81 others
   = note: required for `Point` to implement `Into<JsValue>`
   = note: required for `Point` to implement `IntoJsResult`
   = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `tests` (lib) due to previous error
Error: Compiling your crate to WebAssembly failed
Caused by: failed to execute `cargo build`: exited with exit status: 101
  full command: "cargo" "build" "--lib" "--release" "--target" "wasm32-unknown-unknown"

Both using rustc version 1.67.1 (d5a82bbd2 2023-02-07) and 1.71.0-nightly (8b4b20836 2023-05-22).
Saw a similar issue here but no real solution.

Edit: digging a bit deeper with cargo rustc --profile=check -- -Zunpretty=expanded revealed the following:

without async generates:

#[allow(dead_code)]
pub fn into_js() -> Point { Point { x: 0, y: 0 } }
#[automatically_derived]
const _: () =
    {
        pub unsafe extern "C" fn __wasm_bindgen_generated_into_js()
            -> <Point as wasm_bindgen::convert::ReturnWasmAbi>::Abi {
            let _ret = { let _ret = into_js(); _ret };
            <Point as wasm_bindgen::convert::ReturnWasmAbi>::return_abi(_ret)
        }
    };

whereas with async generates:

#[allow(dead_code)]
pub async fn into_js() -> Point { Point { x: 0, y: 0 } }
#[automatically_derived]
const _: () =
    {
        pub unsafe extern "C" fn __wasm_bindgen_generated_into_js()
            ->
                <wasm_bindgen::JsValue as
                wasm_bindgen::convert::ReturnWasmAbi>::Abi {
            let _ret =
                wasm_bindgen_futures::future_to_promise(async move
                            {
                            {
                                let _ret = into_js();
                                <Point as
                                        wasm_bindgen::__rt::IntoJsResult>::into_js_result(_ret.await)
                            }
                        }).into();
            <wasm_bindgen::JsValue as
                    wasm_bindgen::convert::ReturnWasmAbi>::return_abi(_ret)
        }
    };

Discriminated unions?

I was trying to switch ("match style") over enum cases with fields, but this seems not to be possible. Example (src):

type AppEvent =
  | { kind: "click"; x: number; y: number }
  | { kind: "keypress"; key: string; code: number }
  | { kind: "focus"; element: HTMLElement };

function handleEvent(event: AppEvent) {
  switch (event.kind) {
    case "click":
      // We know it is a mouse click, so we can access `x` and `y` now
      console.log(`Mouse clicked at (${event.x}, ${event.y})`);
      break;
    case "keypress":
      // We know it is a key press, so we can access `key` and `code` now
      console.log(`Key pressed: (key=${event.key}, code=${event.code})`);
      break;
    case "focus":
      // We know it is a focus event, so we can access `element`
      console.log(`Focused element: ${event.element.tagName}`);
      break;
  }
}

Potential feature request, or maybe it's already possible and I'm missing something?

FR: expand rename behaviour when only `into` / `from` + implemented with `rename_all`

Noting behaviour where:

  1. The struct has rename_all(deserialize = "...") or rename_all(serialize = "...")
  2. The tsify macro only generates either into_wasm_abi or from_wasm_abi

With the two, if the rename_all aligns with the single generated variant, one would expect the generated TS code to follow the (e.g.) + into_wasm_abi with the rename_all(serialize = "...") outputs. Based on this, hoping to submit this as a FR to add functionality to enable intrinsic renaming behaviour.

Reproducible Issue

use tsify::Tsify;
use serde::Deserialize;

#[derive(Deserialize, Tsify)]
#[tsify(into_wasm_abi)]
#[serde(rename_all(serialize = "camelCase"))
struct MyStruct {
    should_be_camel_in_ts: i32
}

Ts Outputs

interface MyStruct {
   should_be_camel_in_ts: number
}

Expected

interface MyStruct {
   shouldBeCamelInTs: number
}

Flatten and Option results in broken types

Discovered another strange edge-case today, see the following:

#[test]
fn test_flatten_optional() {
    #[derive(Tsify)]
    struct A {
        a: i32,
        b: String,
    }

    #[derive(Tsify)]
    struct B {
        #[tsify(optional)]
        #[serde(flatten)]
        extra: Option<A>,
        c: i32,
    }

    assert_eq!(
        B::DECL,
        indoc! {""}
    );
}

You get the generated type:

export interface B extends A | null {
    c: number;
}

which is invalid.

I suspect the way to solve this would be for B to become a type and to use & instead of extends so you could do something like:

type B = {

} & (A | {})

I'm not sure of a way to express this using interfaces, so I suspect this would be a pretty substantial change.

Doesn't work for return types if the function is async?

Hi! thanks for creating this library. As the title says, it seems not to work for return types if the exported bindgen function is async. This error shows:

the trait bound `wasm_bindgen::JsValue: From<MyStruct>` is not satisfied

If I make the function non async, it works. For parameters it always works.

Support for `RefFromWasmAbi` and `RefMutFromWasmAbi`

I've just tried out this crate and the typescript interfaces it's creating are awesome. Unfortunately I don't seem to be able to use it in methods like

#[wasm_bindgen]
modify_some_object(my_wasm_obj: &mut MyWasmObj); 

as tsify(into_wasm_abi, from_wasm_abi) doesn't implement support for RefFromWasmAbi and RefMutFromWasmAbi

Is there any plan to support deriving impl's for these?

Type alias instead of interface?

The README gives a nice example of generating interface Point { x: number; y: number; }.

However I would like to generate: type Point = { x: number; y: number; }.

Is there a way to tell tsify that I prefer an alias over an interface?

(The README already talks about aliases for generics, but I'm interested in simple object types, not generics.)

Use of `null` rather than `undefined`

I was running into an issue yesterday with adjacently tagged enums. If you have something like:

#[derive(Debug, Serialize, Deserialize, Tsify)]
#[serde(tag = "reason", content = "inner")]
pub enum ParseBaseUriError {
    MissingTrailingSlash,
    ...
}

Then the generated types have the following structure

declare namespace ParseBaseUriError {
    export type MissingTrailingSlash = { reason: "MissingTrailingSlash"; inner: null };
    ...

The issue here is that the type suggests inner must be null, where in my testing, serializing these objects just leaves the field as undefined.

I've got a branch where I've written a potential fix, but I have a feeling it might be quite hacky and I may not understand the reasoning behind picking null originally. Is there a reason why this library chooses null for unit types rather than undefined? I tried thinking about this and I'm not sure which one I'd pick, as I suspect neither map perfectly.

That branch makes inner optional, while keeping the value of null. If that seems sensible I'm happy to open a PR.

Support untagged variant

Serde supports untagged enum variants (added in this PR):

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Foo {
  Bar,
  Qux,
  #[serde(untagged)]
  Other(f64),
}

This should result in the following TS type:

export type Foo = "bar" | "qux" | number;

However, if you add Tsify to the derive clause, you get an error instead:

unknown serde variant attribute `untagged`

Support for methods?

Probably connected to #3, let's say I have this code:

use wasm_bindgen::prelude::*;

#[derive(serde::Deserialize, tsify::Tsify)]
#[tsify(from_wasm_abi)]
pub struct Foo {
    bar: Bar,
}

#[derive(serde::Deserialize, tsify::Tsify)]
#[tsify(from_wasm_abi)]
pub struct Bar {
    inner: String,
}

#[wasm_bindgen]
impl Bar {
    pub fn foobar(self) -> bool {
        true
    }
}

#[wasm_bindgen]
pub fn foo(foo: Foo) {}

It generates this typescript:

/* tslint:disable */
/* eslint-disable */
/**
* @param {Foo} foo
*/
export function foo(foo: Foo): void;
export interface Foo {
    bar: Bar;
}

export interface Bar {
    inner: string;
}

As you can see, no trace of foobar method. Is it possible to enable methods generation?

Anyway, I can suppose that this code will result in a null pointer exception from second call to foobar, since it consumes self, and that's why I think it's related to #3

How to deal with enum Type union when serde is shared with a key-less format?

Trying to send decoded postcard to js.

Postcard prevent serde to configure enum representation with macro like #[serde(tag = "t", content = "c")] , crashing with a WontSupport error.

The default variant enum then export as:

export type SocketMessage = "Ok" | { Error: { details: string } } | ...

It seems pretty un-usable compared to union with a dedicated key attribute (which type are deduced from key by the editor with if and switch statement).

Any idea to enforce Adjacently tag as wasm_typescript_definition does without using serde(tag = "t", content = "c"?

For the moment i am using a factory to turn the initial struct to another that allow the serde macro.

Invalid references created with `serde(rename = ...)`

When serde(rename = ...) is used the exported type is named to the rename value, but if this type is referenced elsewhere, the original name is used.

e.g.

#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
#[serde(rename = "foo")]
pub struct Foo {
  x: i32,
}


#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
pub struct Bar {
  foo: Foo,
}

produces

export interface foo {
    x: number;
}

export interface Bar {
    foo: Foo;
}

which refers to a non-existent Foo type.

Feature request: `tsify(required)`

I quite often find myself in a spot where I want serde(default) so that old values can be de-serialized, but I don't want the typescript type of be optional. It would be amazing if there was a tsify(required) so that I could do this:

#[tsify(required)]
#[serde(default)]
pub field: String;

which would produce the type:

{
   field: string
}

Or even better, I think I would prefer if #[serde(default)] wouldn't make the field optional in the first place.

Bug: Fields starting with numeric literal need to be quoted

Input

use serde::{Serialize, Deserialize};
use tsify::Tsify;
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi, into_wasm_abi)]
pub struct OneX {
    #[serde(rename = "1x")]
    pub one_x: f64;
}

Output

export interface One {
    1x: number
}

Expected

1x needs to be quoted

export interface One {
    "1x": number
}

Namespaced Recursive Enums generate broken Typescript

When you have a recursive enum such as Foo here:

use tsify::Tsify;
use serde::{Serialize, Deserialize};

#[derive(Tsify)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct OneOf<T> {
    one_of: Vec<T>,
}

#[derive(Tsify)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Bar {}

#[derive(Tsify)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Foo {
    OneOfFoo(OneOf<Foo>),
    Bar(Bar),
}

then the generated Typescript bindings fail to compile

type __FooBar = Bar;
type __FooFoo = Foo;
type __FooOneOf<A> = OneOf<A>;
declare namespace Foo {
    export type OneOfFoo = __FooOneOf<__FooFoo>;
    export type Bar = __FooBar;
}

export type Foo = Foo.OneOfFoo | Foo.Bar;

export interface Bar {}

export interface OneOf<T> {
    oneOf: T[];
}

Due to
TS2456: Type alias 'Foo' circularly references itself.

This does not happen if the type was just

export type Foo = OneOf<Foo> | Bar

I assumed #[serde(untagged)] would provide that but it seems not?

Returning Vec<T> where T: IntoWasmAbi

Hello! Thanks for a great library!

In rustwasm/wasm-bindgen#3554 (See also: rustwasm/wasm-bindgen#111 ) the possibility to return a Vec<SomeType> where SomeType has #[wasm_bindgen] on it was introduced.

Later, in rustwasm/wasm-bindgen#3692 @hillin asked why it wasn't possible to return a Vec<T> where T: IntoWasmAbi. I now have the exact same question. Unfortunately that issue was closed, and moved into this discussion rustwasm/wasm-bindgen#3697 where it received only one reply, from the question asker themself.

The answer was to create a newtype that itself is IntoWasmAbi and then return that:

#[derive(serde::Serialize, tsify::Tsify)]
#[tsify(into_wasm_abi)]
pub struct ThingCollection(Vec<Thing>);

pub fn get_numbers() -> ThingCollection {
   ThingCollection(vec![Thing(1), Thing(2), Thing(3)])
}

Which works, but now I have to have a number of newtypes that I don't want in my code.

I tried improving the situation by creating a generic version of that:

#[derive(serde::Serialize, tsify::Tsify)]
#[tsify(into_wasm_abi)]
pub struct GenericCollection<T: IntoWasmAbi>(Vec<T>);

#[wasm_bindgen(js_class = MyTypeHandle)]
struct MyType {
    #[wasm_bindgen(js_name = getThings)]
    pub fn get_things(&self) -> GenericCollection<Thing> {
        let things = ...;
        GenericCollection(things)
    }
}

Which kind of half-worked - the TS type was correctly generated, but the return value of the function lacked the generic type argument:

export type GenericCollection<T> = T[];

export class MyTypeHandle {
  free(): void;
  getThings(): GenericCollection;
}

So the question is, what's the solution here?

  • Get wasm-bindgen to implement support for Vec<T: IntoWasmAbi>?
  • Get tsify to put the correct generic argument on the ts return type (i.e. getThings(): GenericCollection<Thing>;)?
  • Live with the suboptimal solution?

Allow custom `DECL` TypeScript definition string

Sometimes your type might look like this:

pub struct Color {
  r: i8,
  g: i8,
  b: i8,
}

but you want a custom serde impl + string representation in TypeScript:

// So it can look like #abcdef
export type Color = `#${string}{string}{string}`

Is it possible to generate a TS ambient enum?

Edit: I'm generalizing this issue.

Currently all rust enums are generated as typescript union types. While this is perfectly valid as rust enums are literally discriminated unions, sometimes we'd expect plain old enums in the declaration file. Apparently the d.ts way is to use ambient enum. Can we do this with tsify?

Original issue:
Title: Is it possible to generate a TS string enum?
String enum in Typescript:
https://www.typescriptlang.org/docs/handbook/enums.html#string-enums

Cannot return Tsify types from async methods with wasm-bindgen-futures

The crate tests are failing as per below in many cases on the js feature which means we cannot return rust types in methods and have them automatically cast to JS objects.

-        impl From<JsType> for JsValue {
-            #[inline]
-            fn from(obj: JsType) -> JsValue {
-                obj.obj.into()
-            }
-        }
-        impl JsCast for JsType {
-        ...
-        }

This is broken on both the tests on master (as per ./test.sh) and per the deployed crate.

Let me know if you're aware of a quick fix, or can point me to a likely culprit? Thanks!

Allow Passing Options to serde_wasm_bindgen

serde_wasm_bindgen has various options to change the behavior of the translation between rust and javascript. It would be useful to expose them through attributes on the macro.

Notably:

  • serialize_missing_as_null: Option<T> is T | null instead of T?
  • serialize_maps_as_objects: HashMap<K, V> is Record<K, V> instead of Map<K, V>
  • serialize_large_number_types_as_bigints: {u,i}{64,size} translates to bigint instead of number
  • serialize_bytes_as_arrays: ByteBuf translates to number[] instead of Uint8Array

Tsify doesn't generate typescript definitions under certain conditions

Bug

Sometimes tsify won't generate a TypeScript interface.

How to reproduce

Specifically, under these circumstances:

Given a crate that has exactly one type that has a Tsify derive, and into_wasm_abi but NOT from_wasm_abi:

crates/somecrate/lib.rs

#[derive(Clone, Debug)]
#[derive(tsify::Tsify, serde::Serialize)]
#[tsify(into_wasm_abi)]
pub struct ExampleType {
    pub name: Option<String>,
}

AND the type does not appear in a direct return value (but it does appear in an indirect return value)

my_wasm_lib/lib.rs

#[wasm_bindgen(js_class = ExampleTypeHandleJs)]
impl ExampleTypeHandle {
    #[wasm_bindgen(js_name = getExampleTypeVec)]
    pub fn get_example_type_vec(&self) -> Option<ExampleTypeVec> {
        todo!()
    }
}

#[derive(Tsify, Serialize)]
#[tsify(into_wasm_abi)]
pub struct ExampleTypeVec(pub Vec<ExampleType>);

Then there will be no interface ExampleType in the resulting xxx.d.ts file.

Mitigation

However, this can be mitigated by one or more of the following:

Let the type appear as a direct return type

    #[wasm_bindgen(js_name = getExampleType)]
    pub fn get_example_type(&self) -> ExampleType {
        todo!()
    }

This is solution is unfortunate, because I have no use for such a method.

Let the type use from_wasm_abi

#[derive(Clone, Debug)]
#[derive(tsify::Tsify, serde::Serialize, serde::Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct ExampleType {
    pub name: Option<String>,
}

This solution is unfortunate, because I have no use for from_wasm_abi and serde::Deserialize which just add bloat.

@siefkenj I got a tip from @cwfitzgerald that you have a new fork with recent fixes, would you be open to enabling issues on your fork to get stuffed fixed there?

Support for Generics when using `type = `

If you have a type like this:

#[derive(Tsify)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Foo<T> {
    #[tsify(type = "[T, ...T[]]")]
    bar: Vec<T>,
}

then the generated binding looks like this:

export interface Foo {
    bar: [T, ...T[]];
}

when I would have expected it to be

export interface Foo<T> {
    bar: [T, ...T[]];
}

(and just for clarity, if you don't use the type = macro then it looks like):

export interface Foo<T> {
    bar: T[];
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.