wasm-bindgen icon indicating copy to clipboard operation
wasm-bindgen copied to clipboard

Setting Typescript types for returned duck-typed objects.

Open andreubotella opened this issue 6 years ago • 10 comments

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 avatar Jun 11 '19 19:06 andreubotella

Seems like a reasonable feature to me!

alexcrichton avatar Jun 13 '19 13:06 alexcrichton

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)

Pauan avatar Jun 13 '19 16:06 Pauan

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 }
}

clearloop avatar Feb 02 '20 05:02 clearloop

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.

andreubotella avatar Feb 02 '20 05:02 andreubotella

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

clearloop avatar Feb 02 '20 14:02 clearloop

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;

rcoh avatar Mar 11 '20 16:03 rcoh

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.

stephenmartindale avatar Apr 07 '20 18:04 stephenmartindale

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 https://github.com/rustwasm/wasm-bindgen/issues/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),
}

jkomyno avatar Jun 20 '22 18:06 jkomyno

I'd like to return a Set<number>

paulcdejean avatar Jun 30 '23 01:06 paulcdejean

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.

kevincox avatar Sep 04 '23 22:09 kevincox