Skip to content

Conversation

@FelixNumworks
Copy link
Contributor

@FelixNumworks FelixNumworks commented Sep 11, 2025

This PR adds a param to enum_ to be able to use enums values as plain string or plain numbers in javascript

Int enums:

enum_<Animal>("Animal", enum_value_type::number)
 .value("Dog", Animal::Dog)
 .value("Cat", Animal::Cat)

emits:

// module.d.ts
export type Animal = 1 | 2;

interface EmbindModule {
    Animal: { Dog: 1, Cat: 2 },
}

String enums:

enum_<Animal>("Animal", enum_value_type::string)
 .value("Dog", Animal::Dog)
 .value("Cat", Animal::Cat)

emits:

// module.d.ts
export type Animal = "Dog" | "Cat";

interface EmbindModule {
    Animal: { Dog: "Dog", Cat: "Cat" },
}

Object enums (default):

enum_<Animal>("Animal", enum_value_type::object) 
 .value("Dog", Animal::Dog)
 .value("Cat", Animal::Cat)

 // OR
 Fix https://github.com/emscripten-core/emscripten/issues/24324
Fix https://github.com/emscripten-core/emscripten/issues/19387
Fix https://github.com/emscripten-core/emscripten/issues/18585

enum_<Animal>("Animal") 
 .value("Dog", Animal::Dog)
 .value("Cat", Animal::Cat)

emits:

// module.d.ts
export type AnimalValue<T extends number> {
  value: T
}

export type Animal = AnimalValue<1> | AnimalValue<2>;

interface EmbindModule {
  Animal: { Dog: Animal<1>, Cat: Animal<2> };
}

This doesn't conflict with current implementation of enums, as the parameter default value is the one keeping the same behaviour

Fixes: #24324, #19387, #18585

@FelixNumworks FelixNumworks changed the title feat(embind): add a way to register enums valus as plain string feat(embind): add a way to register enum values as plain string Sep 11, 2025
@kripken kripken requested a review from brendandahl September 11, 2025 22:56
@FelixNumworks
Copy link
Contributor Author

I splitted most of the code between the two, but I kept a common class in the TS types generation, as they are very close to one another

@brendandahl
Copy link
Collaborator

Sorry for the delay here. I got some feedback from a few different projects that also want enum to behave differently and not have a .value. Unfortunately, all the other projects would like the enums in JS to be the integer value, not a string value. Is there reason you want to the string form instead of an int?

On another topic, one downside I see to not using TS enums is you can compare different enums and it will NOT be an error. e.g.

export type Dog = 'a'|'b';
export type Cat = 'a'|'b';

interface EmbindModule {
  Dog: {a: 'a', b: 'b'};
  Cat: {a: 'a', b: 'b'};
};

let module = {} as EmbindModule;

let myDog : Dog = module.Dog.a;

if (myDog == module.Cat.a) { // <--- this is not a compiler error
}

vs

declare enum Dog {
    a = 'a',
    b = 'b',
}

declare enum Cat {
    a = 'a',
    b = 'b',
}

let myDog : Dog = Dog.a;
if (myDog == Cat.a) { < --- compiler error
}

@FelixNumworks
Copy link
Contributor Author

FelixNumworks commented Sep 26, 2025

Why I'm not using TS enums

On another topic, one downside I see to not using TS enums is you can compare different enums and it will NOT be an error. e.g.

I didn't find a clean way to bind the enum to a real TS enum.

If I understand well, you suggest doing:

// module.d.ts
declare enum Animal {
    Dog = 1;
    Cat = 2;
}

But since this enum is only declared in a .d.ts file, it won't be accessible at run time. It only exists as a TS type.

The only way to then make this enum declaration usable, is to add this at the root of module.js:

// module.js, outside the module code

const Animal = {
  Dog: 1,
  Cat: 2,
};

//  Add reverse mapping like real TS enums
Animal[1] = 'Dog';
Animal[2] = 'Cat';

export Animal;

This means that:

  • The enum logic is fully reimplemented in plain js
  • The enum values must be declared outside of the module object (how ?)

I didn't like the idea of manually reimplementing what TS usually does under the hood. And even if I wanted to, I didn't know where to do this implementation. Thus, I went for a plain TS type.

I also think that TS types have the benefit over TS enums of being easier to use and to overload. I prefer using them over TS enums in general (but this is a personal preference. TS enums are a bit clunky imo)

I don't think the comparison problem you raised is that much of an issue. In the end, Dog.a and Cat.a are indeed equal ...

Why I'm using strings

Is there reason you want to the string form instead of an int

We saw above that I couldn't easily implement real TS enums, so I went for:

// module.d.ts
export type Animal = 'Dog' | 'Cat';

interface EmbindModule {
  Animal: { Dog: 'Dog', Cat: 'Cat' },
}

Would you suggest replacing it with the following implementation ? I think it's a bit strange to declare a union type of ints like this..

// module.d.ts
export type Animal = 1 | 2;

interface EmbindModule {
    Animal: { Dog: 1, Cat: 2 },
}

Also this is less close to real TS enums.
Indeed, in TS, you have two ways enums behave:

  • Strings: the enum is "one way"
const enum Animal { Dog: "Dog", Cat, "Cat" };
console.log(Object.values(Animal)); // ["Dog", "Cat"]
  • Numerics: the enum also carries the reverse mapping of values:
const enum Animal { Dog: 1, Cat, 2 };
console.log(Object.values(Animal)); // ["Dog", "Cat", 1, 2]

See https://www.typescriptlang.org/docs/handbook/enums.html for details

Since my implementation only used strings, it was closer to a real TS string enum behaviour.

Finally, using plain strings allows me to use the enum values without even needing the module:

const a: Animal = "Dog"; // << doesn't need the module
// vs
const a: Animal = myModule.Animal.Dog: // << needs an instanciated module

With number I would need to do:

const a: Animal = 1; // << What is 1 ? Dog, Cat ? 
// vs
const a: Animal = myModule.Animal.Dog; // << needs an instanciated module to be readable

Possible solution

If people need the int values, I think we can add a parameter in the enum binding (and merge all implementations inside enum_ for clarity)

Int enums:

enum_<Animal>("Animal", enum_value_type::integer)
 .value("Dog", Animal::Dog)
 .value("Cat", Animal::Cat)

emits:

// module.d.ts
export type Animal = 1 | 2;

interface EmbindModule {
    Animal: { Dog: 1, Cat: 2 },
}

String enums:

enum_<Animal>("Animal", enum_value_type::string)
 .value("Dog", Animal::Dog)
 .value("Cat", Animal::Cat)

emits:

// module.d.ts
export type Animal = "Dog" | "Cat";

interface EmbindModule {
    Animal: { Dog: "Dog", Cat: "Cat" },
}

Legacy enums (default):

enum_<Animal>("Animal", enum_value_type::legacy)
 .value("Dog", Animal::Dog)
 .value("Cat", Animal::Cat)

emits:

// module.d.ts
export type AnimalValue<T extends number> {
  value: T
}

export type Animal = AnimalValue<1> | AnimalValue<2>;

interface EmbindModule {
  Animal: { Dog: Animal<1>, Cat: Animal<2> };
}

This handles the various cases. It's not as close to TS enums, but again I don't think it's that much of a problem.

@FelixNumworks
Copy link
Contributor Author

Hi :) Any new on this @brendandahl ? Do you want me to develop the change I suggested above ?

@brendandahl
Copy link
Collaborator

brendandahl commented Oct 7, 2025

I also can't see a way to make the TS enums work well in a definition file, so the suggested change sounds good. For enum_value_type::legacy, I'd lean towards calling that enum_value_type::default or enum_value_type::value.

I don't think the comparison problem you raised is that much of an issue. In the end, Dog.a and Cat.a are indeed equal ...

FWIW, that misses the point of strong type safety and why the enum class feature was added to c++. Accidently comparing the wrong enums can lead to unexpected behavior.

Finally, using plain strings allows me to use the enum values without even needing the module:

I'd also suggest against doing this. Another value of enums is you can change the underlying values and not have to update your code every place that you hardcoded the value.

@FelixNumworks FelixNumworks force-pushed the string-enums branch 2 times, most recently from 09683fa to ab20b5f Compare October 8, 2025 16:12
Copy link
Collaborator

@brendandahl brendandahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, just a few things to address.

@FelixNumworks FelixNumworks force-pushed the string-enums branch 2 times, most recently from 8f7989d to 2c2feca Compare October 13, 2025 13:44
@FelixNumworks
Copy link
Contributor Author

FelixNumworks commented Oct 13, 2025

Sorry I previously wasn't done with my changes but I pushed early that's why there were some obvious flaws ^^"

I implemented the changes you requested and squashed all the commits together for a cleaner history. (Nothing changed but the tests since last review)

@FelixNumworks
Copy link
Contributor Author

It's still not working. I'll spend more time on this later this week. I'll re-request review when ready

@FelixNumworks FelixNumworks marked this pull request as draft October 13, 2025 15:04
@brendandahl
Copy link
Collaborator

Will you have time to revisit this?

@FelixNumworks
Copy link
Contributor Author

Sorry I didn't have time recently. I'll work on this now and come back today with fixes

@FelixNumworks FelixNumworks force-pushed the string-enums branch 2 times, most recently from dbae6be to 90e28ca Compare December 19, 2025 12:32
@FelixNumworks
Copy link
Contributor Author

The bugs should be fixed. I also enhanced the docs to make it clearer.

@FelixNumworks FelixNumworks marked this pull request as ready for review December 19, 2025 12:33
Copilot AI review requested due to automatic review settings December 19, 2025 12:33
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new enum_value_type parameter to embind's enum_ registration function, allowing C++ enums to be represented in JavaScript as objects (default), plain numbers, or plain strings. This addresses multiple user requests for simpler enum handling.

Key changes:

  • Adds enum_value_type enum class with three options: object (default), number, and string
  • Updates enum registration and TypeScript generation to support all three representation types
  • Adds comprehensive tests demonstrating usage of all three enum types

Reviewed changes

Copilot reviewed 13 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
system/include/emscripten/wire.h Defines the new enum_value_type enum class with three options
system/include/emscripten/bind.h Updates enum_ constructor to accept optional enum_value_type parameter
src/lib/libsigs.js Updates function signature for _embind_register_enum to include new parameter
src/lib/libembind_shared.js Adds helper function to convert raw enum value type to string representation
src/lib/libembind.js Implements runtime behavior for all three enum types with registration and value handling
src/lib/libembind_gen.js Updates TypeScript generation logic to emit appropriate type definitions for each enum type
test/embind/embind_test.cpp Adds test enums and functions for number and string enum types
test/embind/embind.test.js Adds comprehensive test cases for number and string enum behaviors
test/other/embind_tsgen.cpp Refactors test to use three different enum types (FirstEnum, SecondEnum, ThirdEnum)
test/other/embind_tsgen_main.ts Updates test code to use refactored enum names
test/other/embind_tsgen*.d.ts Updates TypeScript definitions to reflect new enum types and their representations
test/test_other.py Adds comment noting test coverage for both default and string enums
site/source/docs/porting/connecting_cpp_and_javascript/embind.rst Adds comprehensive documentation with examples for all three enum types
site/source/docs/api_reference/bind.h.rst Updates API documentation for enum_ constructor with new parameter details

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@FelixNumworks FelixNumworks changed the title feat(embind): add a way to register enum values as plain string feat(embind): add a way to register enum values as number or string Dec 19, 2025
@brendandahl brendandahl enabled auto-merge (squash) December 20, 2025 00:16
@sbc100 sbc100 changed the title feat(embind): add a way to register enum values as number or string [embind] Add a way to register enum values as number or string Dec 20, 2025
auto-merge was automatically disabled December 22, 2025 08:17

Head branch was pushed to by a user without write access

@FelixNumworks
Copy link
Contributor Author

I edited last commit because some tests were failing after merge with main

@brendandahl brendandahl merged commit f5e5d13 into emscripten-core:main Dec 22, 2025
33 of 35 checks passed
@brendandahl
Copy link
Collaborator

Thanks!

@FelixNumworks FelixNumworks deleted the string-enums branch December 22, 2025 18:22
sbc100 pushed a commit that referenced this pull request Dec 22, 2025
brendandahl pushed a commit that referenced this pull request Jan 6, 2026
Following #25257

The main change is that there was an inconsistency in the enum values
between the three examples
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Embind: enums are not serializable. Couldn't the implementation be simplified ?

2 participants