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

Adds support for #[wasm_bindgen(typescript_custom_section)]. #1048

Merged
merged 1 commit into from Nov 26, 2018

Conversation

Projects
None yet
3 participants
@tcr
Copy link
Contributor

tcr commented Nov 24, 2018

Implementation

This diff adds an attribute "typescript_custom_section", inspired by wasm_custom_section. It allows you to annotate a static const string as an addition to wasm-bindgen's generated TypeScript definition files.

#[wasm_bindgen(typescript_custom_section)]
const TS_INTERFACE_EXPORT: &'static str = r"
  interface Square { color: string; width: number; }
";

// Errors
#[wasm_bindgen]
const TS_INTERFACE_EXPORT = r"interface Square { color: string; width: number; } ";
#[wasm_bindgen(typescript_custom_section)]
const TS_INTERFACE_EXPORT: &'static [u8] = [100, 101, 98, 117, 103, 103, 101, 114, 59]
#[wasm_bindgen(typescript_custom_section)]
struct Square { color: String, width: usize }

When wasm-bindgen-cli is invoked with the --typescript parameter, the contents of all typescript_custom_section strings encountered during compilation are appended to the output .d.ts file. This allows Rust to be the source of truth for type definitions in both WebAssembly and Typescript, or route around missing functionality in wasm-bindgen. Most importantly, it enables external tooling to directly generate their own Typescript types.

Motivation

wasm-bindgen exports JavaScript classes and enums that mirror the structure of Rust structs and (C-style) enums. It also exports TypeScript definitions when the --typescript flag is used. For types defined in Rust that you want to share a JSON-type definition for, you may only need to export a TypeScript definition that models it.

For example, sum types can't be exported to TypeScript just using wasm_bindgen:

#[wasm_bindgen]
enum Versions {
  V1,
  V2(String),
  V3{  arg1: bool, arg2: String, },
}

There is no concrete type in JavaScript that corresponds to a tagged union. But if we treat this as plain old JSON-encodable data (as when serializing using serde), we can actually create a Typescript type definition that lives alongside it to express all possible values:

#[wasm_bindgen(typescript_custom_section)]
const TS_VERSIONS_EXPORT: &'static str = r#"

export type Versions =
  | { "tag": "V1" }
  | { "tag": "V2", "fields": string }
  | { "tag": "V3", "fields": { "arg1": bool, "arg2": string } }
  ;

"#;

The primary benefit is ensuring that Rust is the source of truth for shared type definitions (where the alternative is writing your own Typescript definition that must stay in sync with your Rust definition.) But we can leverage even stronger type guarantees, where Typescript gives you semantics that are similar to Rust's tagged unions:

// All matches on a tagged union must be exhausted in TypeScript
switch (version.tag) {
  case 'V1': return 1;
  case 'V2': return 2;
  // would be a typescript error, because you didn't define a 'V3' case
}

Knowing that subcrates might want to define additional Typescript information is the motivating factor for typescript_custom_section. Rather than integrate advanced Typescript generation entirely into wasm-bindgen, we can pull this logic wholly into external libraries.

An example of this is wasm-typescript-definition, a #[derive(TypescriptDefinition)] macro I wrote to try out typescript_custom_section. This enables serde-Serializeable values to export an equivalent TypeScript definition:

#[derive(Serialize, Deserialize, TypescriptDefinition)]
#[serde(tag = "tag", content = "fields")]
enum Versions {
  #[serde(rename = "Version1")]
  V1,
  #[serde(rename = "Version2")]
  V2(String),
  #[serde(rename = "Version3")]
  V3{  arg1: bool, arg2: String, },
}

In this case, the #[derive(TypescriptDefinition)] attribute would generate its own additional #[wasm_bindgen(typescript_custom_section)] section, which would be rolled into your resulting definitions file. It's then able to be used to typecheck user code.

Because this crate is intentionally serde-aware, it can export a Typescript definition that matches the serde encoding exactly. You could deserialize Rust values in Typescript with full typechecking, then return them to Rust with the same guarantee, just by using a type annotation. Using typescript_custom_section, no serde-aware logic needs to be added into wasm-bindgen to make this work.

@alexcrichton

This comment has been minimized.

Copy link
Collaborator

alexcrichton commented Nov 26, 2018

This is a really interesting idea! It seems like a pretty plausible building block to me at least.

@fitzgen you may have thoughts on this as well! I'm sort of tempted to merge as-is, but would like to get a second opinion!

@fitzgen

This comment has been minimized.

Copy link
Member

fitzgen commented Nov 26, 2018

Yeah, this is neat for sure. LGTM!

@alexcrichton

This comment has been minimized.

Copy link
Collaborator

alexcrichton commented Nov 26, 2018

👍 Let's merge then!

@alexcrichton alexcrichton merged commit a202f1c into rustwasm:master Nov 26, 2018

1 check passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
@alexcrichton

This comment has been minimized.

Copy link
Collaborator

alexcrichton commented Nov 26, 2018

@tcr if you're interested as well, I'd love to beef up our TypeScript testing story on CI for wasm-bindgen, and it'd be great to get some help with that!

@tcr

This comment has been minimized.

Copy link
Contributor Author

tcr commented Nov 28, 2018

@alexcrichton I'd love to be able to add Typescript tests :) I'll join the conversation in #922.

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