Skip to content

Support enum variants with values #2407

@joeriexelmans

Description

@joeriexelmans

Hi folks! This feature was discussed in the past in other issues, but it's been quiet for a while and there hasn't been an issue dedicated specifically to this feature, hence this:

Motivation

Right now, only C-style enums are supported. A C-style enum like this

#[wasm_bindgen]
enum CStyleEnum { A, B = 42, C }

is now converted to a mapping from tag to integer values:

const CStyleEnum = Object.freeze({ A: 0, B: 42, C: 43 });

In your code you can just use CStyleEnum.A, like you would in Rust.

Rust's support for Algebraic Data Types (ADTs) is an awesome feature that I love and use intensely. It would be nice if JavaScript bindings could be generated for an enum like this:

#[wasm_bindgen]
enum MyEnum {
    A,
    B(u32),
    C(u32, u32),
    D { x: u32, y: u32 },
}

Proposed Solutions

JavaScript does not have builtin support for "tagged unions" like Rust does, so a decision has to be made on how we "model" this. There was discussion on this in the earlier C-style enum issue, with 2 proposed solutions. I'll summarize them here.

Solution 1: Tag + array of values

This is the most straightforward solution. It reflects Rust's memory layout for enums.

Typescript definition:

type MyEnum =
  | { tag: 'A' }
  | { tag: 'B', value: number }
  | { tag: 'C', value: [number, number] }
  | { tag: 'D', value: { x: number, y: number }
  ;

I like how currently, for C-style enums, a mapping from enum identifier to an integer is created. We could further generalize this by creating a mapping from enum identifier to enum variant constructor:

const MyEnum_Tags = Object.freeze({ A: 0, B: 1, C: 2, D: 3 });
const MyEnum = Object.freeze({
  A: Object.freeze({ tag: MyEnum_Tags.A }), // just a value, like currently for C-style enums
  B: function() { this.tag = MyEnum_Tags.B; this.val = arguments }, // a constructor
  C: function() { this.tag = MyEnum_Tags.C; this.val = arguments },
  D: function() { this.tag = MyEnum_Tags.D; this.val = arguments },
});

and use it as follows:

 // C-style variant. Reflects usage in Rust, and current usage in JS
const a = MyEnum.A;

// Variants with values. Reflects how in Rust a variant with values is a function:
const b = new MyEnum.B(42); 
const c = new MyEnum.C(42, 43);
const d = new MyEnum.D({x: 42, y: 43});

JavaScript allows any function to be called with any list of parameters, so there's no way to prevent e.g. forgetting the number parameter for variant B, doing something like new MyEnum.B(), but that's just the way it is, in a dynamically typed language.

To "match" an enum variant, one can use JS switch-statements:

switch (variant.tag) {
case MyEnum_Tags.A:
  ...
case MyEnum_Tags.B:
  ...
}

if-else:

if (variant.tag === MyEnum_Tags.C) {
   const [x, y] = variant.val; // this is not too pretty, but values must be put under a field ("val"), in order to prevent naming collisions with the "tag" field.
   ...
}

or use a mapping:

const something = {
  [MyEnum_Tags.A]: ... ,
  [MyEnum_Tags.B]: ... ,
}[variant.tag];

Alternatives

Solution 2: Keep the tag as a key in the object

The following was also proposed:

type MyEnum =
  | { A: any }
  | { B: { '0': number } }
  | { C: { '0': number, '1': number } }
  | { D: { x: number, y: number } }
  ;

I'm personally not in favor, because one is forced to add a dummy value for variant 'A', which is inefficient:

const variant = { A: {} };

And this dummy value has to be truthy, in order for

if (variant.A) { ... }

to match.

Also, I think checking if an object contains a key is less efficient in (non-optimized?) JavaScript, compared to just checking the value of an integer field ("tag").

To "match" a variant, to my knowledge, one can only use if-statements.

if (variant.C) {
  const [x, y] = variant.C;
  ...
}

Additional info

I'm willing to put some work into this. Currently looking at the source of the wasm_bindgen macro to see if I can implement it myself.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions