Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setting Typescript types for returned duck-typed objects. #1591

Open
andreubotella opened this issue Jun 11, 2019 · 10 comments
Open

Setting Typescript types for returned duck-typed objects. #1591

andreubotella opened this issue Jun 11, 2019 · 10 comments

Comments

@andreubotella
Copy link

andreubotella commented Jun 11, 2019

Motivation

As far as I understand, the only way to return an object with a duck-typed interface from Rust to JS for the time being is to build it by reflection and return it as a JsValue. The generated Typescript definition rightfully gives the returned type as any, which does its job, but it fails to convey the author's intentions. A better solution would be to add a way to declare a Typescript type for a returned JsValue.

Proposed Solution

The proposed solution would be to add a wasm_bindgen attribute, perhaps called something like return_typescript_type, containing a Typescript type. Any named references contained in the type must be defined elsewhere in a typescript_custom_section, although parsing the TS to ensure that might not be a priority.

#[wasm_bindgen(js_name = jpegImageSize, return_typescript_type = {width: number, height: number}]
pub fn jpeg_image_size(image: &[u8]) -> JsValue {
   let (width, height): (u32, u32) = unimplemented!();
   let ret = Object::new();
   Reflect::set(&ret, &JsValue::from_str("width"), &JsValue::from_f64(width as f64);
   Reflect::set(&ret, &JsValue::from_str("height"), &JsValue::from_f64(height as f64);
   ret
}

Alternatives

Hopefully sometime in the future, wasm_bindgen will offer a way to return duck-typed interfaces from Rust without reflection, which would provide the necessary types for the TS declaration to be reasonably complete by default.

@andreubotella andreubotella added the enhancement New feature or request label Jun 11, 2019
@alexcrichton alexcrichton added typescript and removed enhancement New feature or request labels Jun 13, 2019
@alexcrichton
Copy link
Contributor

Seems like a reasonable feature to me!

@Pauan
Copy link
Contributor

Pauan commented Jun 13, 2019

Currently you can just return a Rust struct, like so:

#[wasm_bindgen]
pub struct Foo {
    pub width: f64,
    pub height: f64,
}

#[wasm_bindgen]
pub fn jpeg_image_size(image: &[u8]) -> Foo {
    let (width, height): (u32, u32) = unimplemented!();
    Foo { width, height }
}

This also generates correct TypeScript types. The only issue is it creates a JS class, so I think it would be cool to have a plain_object or similar attribute that would cause it to instead generate a plain JS object:

#[wasm_bindgen(plain_object)]
pub struct Foo {
    pub width: f64,
    pub height: f64,
}

(Obviously methods wouldn't work on it, and there's some subtlety around free)

@clearloop
Copy link
Contributor

clearloop commented Feb 2, 2020

Any progress?

What if using #[wasm_bindgen(typescript_custom_section)], and ref the type defination to the output JsValue?

Depends on chapter 2.9:

/// exports type `Pink` to `.d.ts`
#[wasm_bindgen(typescript_custom_section)]
const TS_APPEND_CONTENT: &'static str = r#"
export type Pink = { "whoami": string }; 
"#;

/// duck-type interface
#[wasm_bindgen]
extern "C" {
    pub type Foo;

    #[wasm_bindgen(structural, method, getter)]
    pub fn width(this: &Foo) -> String;

    #[wasm_bindgen(structural, method, getter)]
    pub fn height(this: &Foo) -> String;
}

/// generate `Foo` to `.d.ts`
#[wasm_bindgen]
pub fn jpeg_image_size(image: &[u8]) -> Foo {
    let (width, height): (u32, u32) = unimplemented!();
    Foo { width, height }
}

@andreubotella
Copy link
Author

andreubotella commented Feb 2, 2020

Any progress?

I'd moved on from this long ago, following @Pauan's suggestion I believe. I'd tackle this now that you've reminded me, but I'm not familiar with the wasm-bindgen code or have a lot of time to invest in it. I don't think it'd be hard to do in any case, it'd probably qualify as a good first issue.

@clearloop
Copy link
Contributor

@andreubotella Thanks for ur advice, this is a good entry for me to contribute to wasm-bindgen, I'll try to handle this.

@rcoh
Copy link

rcoh commented Mar 11, 2020

Since the plain-object won't work with Vecs, I'd rather having something like

struct Bar { ... }
struct Foo {
  bars: Vec<Bar>
}
#[wasm_bindgen]
fn foo() -> JsValue<Foo> {
  //
}
interface Bar {
 ...
}
interface Foo {
  bars: Array<Bar>
}

foo(): Foo;

@stephenmartindale
Copy link

For me, the first-prize solution would be for us to be able to declare the a struct can be marshalled to Javascript in one direction, only.

For example:

#[wasm_bindgen(serialize-only)]
struct DataPack {
   anumber: i32,
   anarray: Vec<i32>,
   aboxedslice: Box<[(i32, i32, i32)]>,
}

This would have the same effect, conceptually, as making the struct serializable with serde and marshalling it via JsValue (preferably via the serde-wasm-bindgen crate because that's cheaper) except that it would allow wasm-bindgen to do all the leg work for us and allow us to use readable types in our methods:

   pub fn get_the_data(&self) -> DataPack {
   ...
   }

Additional benefits:

  • No need to worry about arrays, vectors or collections because anything iterable can be marshalled to a plain array if one-way marshalling can be assumed.
  • No need to worry about anything that isn't state because there's no way to call a method on something that marshals one way.
  • Covers 100% of the friction that I, personally, experience with wasm-bindgen.
    • For the reverse case, using an extern to expose a Javascript type to Rust is a tonne easier than this serde -> JsValue time-sink.
    • My project runs on Rust in the background, providing a TypeScript U.I. This means that 99% of my marshalling needs are one-way: TypeScript needs to read something in order to draw it or update a control.
  • wasm-bindgen can automatically create strong TypeScript definitions for the types.
  • No more JsValue in signatures.

@jkomyno
Copy link

jkomyno commented Jun 20, 2022

I appreciate @clearloop's contribution to this issue. The problem, however, is that we still cannot use #[wasm_bindgen(typescript_type = "...")] for structs that use features not allowed by wasm-bindgen, like the one mentioned in #2407:

#[wasm_bindgen(typescript_custom_section)]
const TS_APPEND_CONTENT: &'static str = r#"
type DatasourceDbURL
  = { static: string }
  | { env: string }
"#;

#[wasm_bindgen(typescript_type = "DatasourceDb")]
pub enum DatasourceDbURL {
    Static(String),
//  ^^^^^^^^^^^^^^
//  only C-Style enums allowed with #[wasm_bindgen]

    Env(String),
}

@paulcdejean
Copy link

paulcdejean commented Jun 30, 2023

I'd like to return a Set<number>

@kevincox
Copy link

kevincox commented Sep 4, 2023

A decent workaround to this is to skip automatically emitting the type and add it manually. This is error-prone but not too much boilerplate.

// Define TypeScript interfaces for your serializable types.
#[wasm_bindgen(typescript_custom_section)]
const TYPES: &str = r###"
	interface Position {
		x: number,
		y: number,
	}
"###;

#[wasm_bindgen]
pub struct Step(...);

#[wasm_bindgen]
impl Step {
	// Apply skip_typescript to the methods that return JsValue.
	#[wasm_bindgen(skip_typescript)]
	pub fn trace(&mut self) -> JsValue {
		serde_wasm_bindgen::to_value(&self.0.trace()).unwrap()
	}
}

// Manually add the method to the interface.
#[wasm_bindgen(typescript_custom_section)]
const STEP_TYPES: &str = r###"
	export interface Step {
		trace(): Position[];
	}
"###;

Hopefully we can get proper support for emitting proper types for these "JSON" values soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants