Skip to content

Commit 4c2e923

Browse files
Generate TS types for string enums (#4180)
1 parent 03c8e2d commit 4c2e923

File tree

18 files changed

+128
-30
lines changed

18 files changed

+128
-30
lines changed

CHANGELOG.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33

44
## Unreleased
55

6+
### Changed
7+
8+
* String enums now generate private TypeScript types but only if used.
9+
[#4174](https://github.com/rustwasm/wasm-bindgen/pull/4174)
10+
611
### Fixed
712

8-
* Fixed methods with `self: &Self` consuming the object
13+
* Fixed methods with `self: &Self` consuming the object.
914
[#4178](https://github.com/rustwasm/wasm-bindgen/pull/4178)
1015

1116
--------------------------------------------------------------------------------

crates/backend/src/ast.rs

+4
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,12 @@ pub struct StringEnum {
343343
pub variants: Vec<Ident>,
344344
/// The JS string values of the variants
345345
pub variant_values: Vec<String>,
346+
/// The doc comments on this enum, if any
347+
pub comments: Vec<String>,
346348
/// Attributes to apply to the Rust enum
347349
pub rust_attrs: Vec<syn::Attribute>,
350+
/// Whether to generate a typescript definition for this enum
351+
pub generate_typescript: bool,
348352
/// Path to wasm_bindgen
349353
pub wasm_bindgen: Path,
350354
}

crates/backend/src/encode.rs

+2
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,9 @@ fn shared_import_type<'a>(i: &'a ast::ImportType, intern: &'a Interner) -> Impor
362362
fn shared_import_enum<'a>(i: &'a ast::StringEnum, _intern: &'a Interner) -> StringEnum<'a> {
363363
StringEnum {
364364
name: &i.js_name,
365+
generate_typescript: i.generate_typescript,
365366
variant_values: i.variant_values.iter().map(|x| &**x).collect(),
367+
comments: i.comments.iter().map(|s| &**s).collect(),
366368
}
367369
}
368370

crates/cli-support/src/js/binding.rs

+29-11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::js::Context;
88
use crate::wit::InstructionData;
99
use crate::wit::{Adapter, AdapterId, AdapterKind, AdapterType, Instruction};
1010
use anyhow::{anyhow, bail, Error};
11+
use std::collections::HashSet;
1112
use std::fmt::Write;
1213
use walrus::{Module, ValType};
1314

@@ -68,6 +69,7 @@ pub struct JsFunction {
6869
pub js_doc: String,
6970
pub ts_arg_tys: Vec<String>,
7071
pub ts_ret_ty: Option<String>,
72+
pub ts_refs: HashSet<TsReference>,
7173
/// Whether this function has a single optional argument.
7274
///
7375
/// If the function is a setter, that means that the field it sets is optional.
@@ -76,6 +78,14 @@ pub struct JsFunction {
7678
pub log_error: bool,
7779
}
7880

81+
/// A references to an (likely) exported symbol used in TS type expression.
82+
///
83+
/// Right now, only string enum require this type of anaylsis.
84+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85+
pub enum TsReference {
86+
StringEnum(String),
87+
}
88+
7989
impl<'a, 'b> Builder<'a, 'b> {
8090
pub fn new(cx: &'a mut Context<'b>) -> Builder<'a, 'b> {
8191
Builder {
@@ -246,7 +256,7 @@ impl<'a, 'b> Builder<'a, 'b> {
246256
// should start from here. Struct fields(Getter) only have one arg, and
247257
// this is the clue we can infer if a function might be a field.
248258
let mut might_be_optional_field = false;
249-
let (ts_sig, ts_arg_tys, ts_ret_ty) = self.typescript_signature(
259+
let (ts_sig, ts_arg_tys, ts_ret_ty, ts_refs) = self.typescript_signature(
250260
&function_args,
251261
&arg_tys,
252262
&adapter.inner_results,
@@ -266,6 +276,7 @@ impl<'a, 'b> Builder<'a, 'b> {
266276
js_doc,
267277
ts_arg_tys,
268278
ts_ret_ty,
279+
ts_refs,
269280
might_be_optional_field,
270281
catch: self.catch,
271282
log_error: self.log_error,
@@ -285,11 +296,12 @@ impl<'a, 'b> Builder<'a, 'b> {
285296
might_be_optional_field: &mut bool,
286297
asyncness: bool,
287298
variadic: bool,
288-
) -> (String, Vec<String>, Option<String>) {
299+
) -> (String, Vec<String>, Option<String>, HashSet<TsReference>) {
289300
// Build up the typescript signature as well
290301
let mut omittable = true;
291302
let mut ts_args = Vec::new();
292303
let mut ts_arg_tys = Vec::new();
304+
let mut ts_refs = HashSet::new();
293305
for (name, ty) in arg_names.iter().zip(arg_tys).rev() {
294306
// In TypeScript, we can mark optional parameters as omittable
295307
// using the `?` suffix, but only if they're not followed by
@@ -301,12 +313,12 @@ impl<'a, 'b> Builder<'a, 'b> {
301313
match ty {
302314
AdapterType::Option(ty) if omittable => {
303315
arg.push_str("?: ");
304-
adapter2ts(ty, &mut ts);
316+
adapter2ts(ty, &mut ts, Some(&mut ts_refs));
305317
}
306318
ty => {
307319
omittable = false;
308320
arg.push_str(": ");
309-
adapter2ts(ty, &mut ts);
321+
adapter2ts(ty, &mut ts, Some(&mut ts_refs));
310322
}
311323
}
312324
arg.push_str(&ts);
@@ -342,7 +354,7 @@ impl<'a, 'b> Builder<'a, 'b> {
342354
let mut ret = String::new();
343355
match result_tys.len() {
344356
0 => ret.push_str("void"),
345-
1 => adapter2ts(&result_tys[0], &mut ret),
357+
1 => adapter2ts(&result_tys[0], &mut ret, Some(&mut ts_refs)),
346358
_ => ret.push_str("[any]"),
347359
}
348360
if asyncness {
@@ -351,7 +363,7 @@ impl<'a, 'b> Builder<'a, 'b> {
351363
ts.push_str(&ret);
352364
ts_ret = Some(ret);
353365
}
354-
(ts, ts_arg_tys, ts_ret)
366+
(ts, ts_arg_tys, ts_ret, ts_refs)
355367
}
356368

357369
/// Returns a helpful JS doc comment which lists types for all parameters
@@ -374,7 +386,7 @@ impl<'a, 'b> Builder<'a, 'b> {
374386
for (name, ty) in fn_arg_names.iter().zip(arg_tys).rev() {
375387
let mut arg = "@param {".to_string();
376388

377-
adapter2ts(ty, &mut arg);
389+
adapter2ts(ty, &mut arg, None);
378390
arg.push_str("} ");
379391
match ty {
380392
AdapterType::Option(..) if omittable => {
@@ -395,7 +407,7 @@ impl<'a, 'b> Builder<'a, 'b> {
395407

396408
if let (Some(name), Some(ty)) = (variadic_arg, arg_tys.last()) {
397409
ret.push_str("@param {...");
398-
adapter2ts(ty, &mut ret);
410+
adapter2ts(ty, &mut ret, None);
399411
ret.push_str("} ");
400412
ret.push_str(name);
401413
ret.push('\n');
@@ -1416,7 +1428,7 @@ impl Invocation {
14161428
}
14171429
}
14181430

1419-
fn adapter2ts(ty: &AdapterType, dst: &mut String) {
1431+
fn adapter2ts(ty: &AdapterType, dst: &mut String, refs: Option<&mut HashSet<TsReference>>) {
14201432
match ty {
14211433
AdapterType::I32
14221434
| AdapterType::S8
@@ -1434,13 +1446,19 @@ fn adapter2ts(ty: &AdapterType, dst: &mut String) {
14341446
AdapterType::Bool => dst.push_str("boolean"),
14351447
AdapterType::Vector(kind) => dst.push_str(&kind.js_ty()),
14361448
AdapterType::Option(ty) => {
1437-
adapter2ts(ty, dst);
1449+
adapter2ts(ty, dst, refs);
14381450
dst.push_str(" | undefined");
14391451
}
14401452
AdapterType::NamedExternref(name) => dst.push_str(name),
14411453
AdapterType::Struct(name) => dst.push_str(name),
14421454
AdapterType::Enum(name) => dst.push_str(name),
1443-
AdapterType::StringEnum => dst.push_str("any"),
1455+
AdapterType::StringEnum(name) => {
1456+
if let Some(refs) = refs {
1457+
refs.insert(TsReference::StringEnum(name.clone()));
1458+
}
1459+
1460+
dst.push_str(name);
1461+
}
14441462
AdapterType::Function => dst.push_str("any"),
14451463
}
14461464
}

crates/cli-support/src/js/mod.rs

+30
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::wit::{AuxEnum, AuxExport, AuxExportKind, AuxImport, AuxStruct};
99
use crate::wit::{JsImport, JsImportName, NonstandardWitSection, WasmBindgenAux};
1010
use crate::{reset_indentation, Bindgen, EncodeInto, OutputMode, PLACEHOLDER_MODULE};
1111
use anyhow::{anyhow, bail, Context as _, Error};
12+
use binding::TsReference;
1213
use std::borrow::Cow;
1314
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
1415
use std::fmt;
@@ -47,6 +48,10 @@ pub struct Context<'a> {
4748
/// identifiers.
4849
defined_identifiers: HashMap<String, usize>,
4950

51+
/// A set of all (tracked) symbols referenced from within type definitions,
52+
/// function signatures, etc.
53+
typescript_refs: HashSet<TsReference>,
54+
5055
exported_classes: Option<BTreeMap<String, ExportedClass>>,
5156

5257
/// A map of the name of npm dependencies we've loaded so far to the path
@@ -108,6 +113,7 @@ impl<'a> Context<'a> {
108113
js_imports: Default::default(),
109114
defined_identifiers: Default::default(),
110115
wasm_import_definitions: Default::default(),
116+
typescript_refs: Default::default(),
111117
exported_classes: Some(Default::default()),
112118
config,
113119
threads_enabled: config.threads.is_enabled(module),
@@ -2662,6 +2668,7 @@ __wbg_set_wasm(wasm);"
26622668
ts_sig,
26632669
ts_arg_tys,
26642670
ts_ret_ty,
2671+
ts_refs,
26652672
js_doc,
26662673
code,
26672674
might_be_optional_field,
@@ -2688,6 +2695,8 @@ __wbg_set_wasm(wasm);"
26882695
Kind::Adapter => "failed to generates bindings for adapter".to_string(),
26892696
})?;
26902697

2698+
self.typescript_refs.extend(ts_refs);
2699+
26912700
// Once we've got all the JS then put it in the right location depending
26922701
// on what's being exported.
26932702
match kind {
@@ -3822,6 +3831,27 @@ __wbg_set_wasm(wasm);"
38223831
.map(|v| format!("\"{v}\""))
38233832
.collect();
38243833

3834+
if string_enum.generate_typescript
3835+
&& self
3836+
.typescript_refs
3837+
.contains(&TsReference::StringEnum(string_enum.name.clone()))
3838+
{
3839+
let docs = format_doc_comments(&string_enum.comments, None);
3840+
3841+
self.typescript.push_str(&docs);
3842+
self.typescript.push_str("type ");
3843+
self.typescript.push_str(&string_enum.name);
3844+
self.typescript.push_str(" = ");
3845+
3846+
if variants.is_empty() {
3847+
self.typescript.push_str("never");
3848+
} else {
3849+
self.typescript.push_str(&variants.join(" | "));
3850+
}
3851+
3852+
self.typescript.push_str(";\n");
3853+
}
3854+
38253855
self.global(&format!(
38263856
"const __wbindgen_enum_{name} = [{values}];\n",
38273857
name = string_enum.name,

crates/cli-support/src/wit/incoming.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ impl InstructionBuilder<'_, '_> {
110110
},
111111
Descriptor::StringEnum { name, invalid, .. } => {
112112
self.instruction(
113-
&[AdapterType::StringEnum],
113+
&[AdapterType::StringEnum(name.clone())],
114114
Instruction::StringEnumToWasm {
115115
name: name.clone(),
116116
invalid: *invalid,
@@ -312,7 +312,7 @@ impl InstructionBuilder<'_, '_> {
312312
hole,
313313
} => {
314314
self.instruction(
315-
&[AdapterType::StringEnum.option()],
315+
&[AdapterType::StringEnum(name.clone()).option()],
316316
Instruction::OptionStringEnumToWasm {
317317
name: name.clone(),
318318
invalid: *invalid,

crates/cli-support/src/wit/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -868,11 +868,13 @@ impl<'a> Context<'a> {
868868
fn string_enum(&mut self, string_enum: &decode::StringEnum<'_>) -> Result<(), Error> {
869869
let aux = AuxStringEnum {
870870
name: string_enum.name.to_string(),
871+
comments: concatenate_comments(&string_enum.comments),
871872
variant_values: string_enum
872873
.variant_values
873874
.iter()
874875
.map(|v| v.to_string())
875876
.collect(),
877+
generate_typescript: string_enum.generate_typescript,
876878
};
877879
let mut result = Ok(());
878880
self.aux

crates/cli-support/src/wit/nonstandard.rs

+4
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,12 @@ pub struct AuxEnum {
179179
pub struct AuxStringEnum {
180180
/// The name of this enum
181181
pub name: String,
182+
/// The copied Rust comments to forward to JS
183+
pub comments: String,
182184
/// A list of variants values
183185
pub variant_values: Vec<String>,
186+
/// Whether typescript bindings should be generated for this enum.
187+
pub generate_typescript: bool,
184188
}
185189

186190
#[derive(Debug)]

crates/cli-support/src/wit/outgoing.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ impl InstructionBuilder<'_, '_> {
295295
name: name.clone(),
296296
hole: *hole,
297297
},
298-
&[AdapterType::StringEnum.option()],
298+
&[AdapterType::StringEnum(name.clone()).option()],
299299
);
300300
}
301301
Descriptor::RustStruct(name) => {
@@ -538,7 +538,7 @@ impl InstructionBuilder<'_, '_> {
538538
Instruction::WasmToStringEnum {
539539
name: name.to_string(),
540540
},
541-
&[AdapterType::StringEnum],
541+
&[AdapterType::StringEnum(name.to_string())],
542542
);
543543
}
544544

crates/cli-support/src/wit/standard.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ pub enum AdapterType {
8686
Option(Box<AdapterType>),
8787
Struct(String),
8888
Enum(String),
89-
StringEnum,
89+
StringEnum(String),
9090
NamedExternref(String),
9191
Function,
9292
NonNull,

crates/cli/tests/reference/enums.d.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ export function enum_echo(color: Color): Color;
1212
export function option_enum_echo(color?: Color): Color | undefined;
1313
/**
1414
* @param {Color} color
15-
* @returns {any}
15+
* @returns {ColorName}
1616
*/
17-
export function get_name(color: Color): any;
17+
export function get_name(color: Color): ColorName;
1818
/**
19-
* @param {any | undefined} [color]
20-
* @returns {any | undefined}
19+
* @param {ColorName | undefined} [color]
20+
* @returns {ColorName | undefined}
2121
*/
22-
export function option_string_enum_echo(color?: any): any | undefined;
22+
export function option_string_enum_echo(color?: ColorName): ColorName | undefined;
2323
/**
2424
* A color.
2525
*/
@@ -43,3 +43,7 @@ export enum ImplicitDiscriminant {
4343
C = 42,
4444
D = 43,
4545
}
46+
/**
47+
* The name of a color.
48+
*/
49+
type ColorName = "green" | "yellow" | "red";

crates/cli/tests/reference/enums.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,16 @@ export function option_enum_echo(color) {
4646

4747
/**
4848
* @param {Color} color
49-
* @returns {any}
49+
* @returns {ColorName}
5050
*/
5151
export function get_name(color) {
5252
const ret = wasm.get_name(color);
5353
return __wbindgen_enum_ColorName[ret];
5454
}
5555

5656
/**
57-
* @param {any | undefined} [color]
58-
* @returns {any | undefined}
57+
* @param {ColorName | undefined} [color]
58+
* @returns {ColorName | undefined}
5959
*/
6060
export function option_string_enum_echo(color) {
6161
const ret = wasm.option_string_enum_echo(isLikeNone(color) ? 4 : ((__wbindgen_enum_ColorName.indexOf(color) + 1 || 4) - 1));
@@ -83,6 +83,10 @@ export const ImplicitDiscriminant = Object.freeze({ A:0,"0":"A",B:1,"1":"B",C:42
8383

8484
const __wbindgen_enum_ColorName = ["green", "yellow", "red"];
8585

86+
const __wbindgen_enum_FooBar = ["foo", "bar"];
87+
88+
const __wbindgen_enum_PrivateStringEnum = ["foo", "bar"];
89+
8690
export function __wbindgen_throw(arg0, arg1) {
8791
throw new Error(getStringFromWasm0(arg0, arg1));
8892
};

0 commit comments

Comments
 (0)