Setting Typescript types for returned duck-typed objects.
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.
Seems like a reasonable feature to me!
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)
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 }
}
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 Thanks for ur advice, this is a good entry for me to contribute to wasm-bindgen, I'll try to handle this.
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;
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
externto 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.
- For the reverse case, using an
- wasm-bindgen can automatically create strong TypeScript definitions for the types.
- No more
JsValuein signatures.
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),
}
I'd like to return a Set<number>
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.