Suppose you are wanting to use Emscripten's Embind with an Enum.
// exif-byte-order.h
typedef enum {
EXIF_BYTE_ORDER_MOTOROLA,
EXIF_BYTE_ORDER_INTEL
} ExifByteOrder;
// .cpp
#include <libexif/exif-byte-order.h>
#include <emscripten/bind.h>
using namespace emscripten;
EMSCRIPTEN_BINDINGS(Enum) {
enum_<ExifByteOrder>("ExifByteOrder")
.value("MOTOROLA", EXIF_BYTE_ORDER_MOTOROLA)
.value("INTEL", EXIF_BYTE_ORDER_INTEL)
;
}
This would be the resulting declaration TypeScript file.
// module.d.ts
export interface ExifByteOrderValue<T extends number> {
value: T;
}
export type ExifByteOrder = ExifByteOrderValue<0>|ExifByteOrderValue<1>;
interface EmbindModule {
ExifByteOrder: {MOTOROLA: ExifByteOrderValue<0>, INTEL: ExifByteOrderValue<1>};
}
So far so good, let's check what the console tells us:
console.log("ExifByteOrder", ExifByteOrder);
console.log("typeof ExifByteOrder", typeof ExifByteOrder);
console.log("Object.keys(ExifByteOrder)", Object.keys(ExifByteOrder));
console.log("ExifByteOrder['MOTOROLA'].value", ExifByteOrder["MOTOROLA"].value);
console.log("ExifByteOrder['INTEL'].name", ExifByteOrder["INTEL"].value);
❯ node index.js
ExifByteOrder [Function: ctor] {
values: { '0': ctor {}, '1': ctor {} },
argCount: undefined,
MOTOROLA: ctor {},
INTEL: ctor {}
}
typeof ExifByteOrder function
Object.keys(ExifByteOrder) [ 'values', 'argCount', 'MOTOROLA', 'INTEL' ]
ExifByteOrder['MOTOROLA'].value 0
ExifByteOrder['INTEL'].name 1
...huh? I have no idea why it's a function (long shot guess, it's because WASM can only export functions and WebAssembly.Memory) but judging by the name "ctor", I'd guess it has something to do with the EVAL_CTORS
option. Moreover, the argCount and values property do intrigue me.
Since it is a function, let's see how it reacts to typical function behavior (despite TypeScript's insisitence on it not existing)
console.log("ExifByteOrder.length", ExifByteOrder.length);
console.log("ExifByteOrder.name", ExifByteOrder.name);
console.log("ExifByteOrder.prototype", ExifByteOrder.prototype);
console.log("ExifByteOrder()", ExifByteOrder());
console.log("ExifByteOrder.toString()", ExifByteOrder.toString());
❯ node index.js
ExifByteOrder.length 0
ExifByteOrder.name ctor
ExifByteOrder.prototype {}
ExifByteOrder() undefined
ExifByteOrder.toString() function ctor() {}
I'm going to assume at this point the pattern for the JavaScript is obvious and only show the output:
❯ node index.js
ExifByteOrder.values { '0': ctor {}, '1': ctor {} }
typeof ExifByteOrder.values object
ExifByteOrder.values['1'] ctor {}
typeof ExifByteOrder.values['1'] object
ExifByteOrder['MOTOROLA'] ctor {}
typeof ExifByteOrder['MOTOROLA'] object
ExifByteOrder['INTEL'] ctor {}
typeof ExifByteOrder['INTEL'] object
Interesting how they're all objects. Let's see what values they have.
❯ node index.js
Object.entries(ExifByteOrder.values) [ [ '0', ctor {} ], [ '1', ctor {} ] ]
Object.entries(ExifByteOrder.values['1']) []
Object.entries(ExifByteOrder['MOTOROLA']) []
Object.entries(ExifByteOrder['INTEL']) []
Ok, so nothing, perhaps it has non-enumerable properties?
❯ node index.js
Object.getOwnPropertyDescriptors(ExifByteOrder.values) {
'0': {
value: ctor {},
writable: true,
enumerable: true,
configurable: true
},
'1': {
value: ctor {},
writable: true,
enumerable: true,
configurable: true
}
}
Object.getOwnPropertyDescriptors(ExifByteOrder.values['1']) {
value: { value: 1, writable: false, enumerable: false, configurable: false },
constructor: {
value: [Function: ExifByteOrder_INTEL],
writable: false,
enumerable: false,
configurable: false
}
}
Object.getOwnPropertyDescriptors(ExifByteOrder['MOTOROLA']) {
value: { value: 0, writable: false, enumerable: false, configurable: false },
constructor: {
value: [Function: ExifByteOrder_MOTOROLA],
writable: false,
enumerable: false,
configurable: false
}
}
Object.getOwnPropertyDescriptors(ExifByteOrder['INTEL']) {
value: { value: 1, writable: false, enumerable: false, configurable: false },
constructor: {
value: [Function: ExifByteOrder_INTEL],
writable: false,
enumerable: false,
configurable: false
}
}
Well... neat. I have no idea what this means. But anyway, if you're looking for code to make this look more like a reasonable enum or object (though you lose the reverse mapping which might be what the .values is for...? I should've tested it with an enum that had better values)
/**
* Before the C23 standard, enums were not allowed to have a value that was not
* an `int`
*
* @see {@link https://open-std.org/JTC1/SC22/WG14/www/docs/n3029.htm}
*/
interface EmbindEnumValue<T> {
value: T;
}
/**
* Maps an enum generated from Emscripten's Embind to a plain object with the
* key-value pairs from the enum
*
* @see {@link https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#enums}
* @see {@link https://github.com/emscripten-core/emscripten/blob/main/src/lib/libembind_gen.js#L277-L310}
*/
const mapEmbindEnumToObject = <
Enum extends Record<PropertyKey, EmbindEnumValue<unknown>>,
>(
embindEnum: Enum,
) =>
Object.fromEntries(
Object.entries(embindEnum)
.filter(([key]) => !["values", "argCount"].includes(key))
.map(([key, value]) => [key, value.value]),
) as {
[K in keyof Enum]: Enum[K] extends { value: infer V } ? V : never;
};
export { mapEmbindEnumToObject };
I am really not a fan of the cast but there's really no way around it using Object.fromEntries()
. If you wanted runtime error handling you probably should check if typeof enumObj === 'function'
.
I don't know if this behavior is expected or will change in the future. Currently the docs only have two sentences for Enums, those being
Embind’s enumeration support works with both C++98 enums and C++11 “enum classes”. In both cases, JavaScript accesses enumeration values as properties of the type.
I've been thinking a lot about this and there's a lot of quirks and pain points with what I've been doing. I may elaborate further on this in the future, but for now, let me try a slightly more complicated version of my
mapEmbindEnumToObject
function.The largest changes are:
The idea is to bring it closer in line with the ECMAScript proposal for Enums (see tc39/proposal-enum). It's not perfect, namely [Symbol.toStringTag] being the name of the enum I don't think is possible, but the changes fall in line with the proposed standard.
Making it iterable is actually very useful since doing reverse lookups or iterating through all members of enum in my experience occurred very frequently, and now it is a lot more simplified by just using
Array.from(enum)
instead of usingObject.entries
repeatedly. However, this does mean that instead of being able to usekeyof typeof Enum
, you do have to do something liketype EnumKey = Exclude<keyof typeof Enum, symbol>
but I think that's a fine tradeoff.The null prototype and non-extensibility is just to bring it in line with the proposal. I would like to say so that this could be a drop-in change at some point in the future when it is implemented, but since it is currently in Stage 1 and was last presented April 2025, I suspect we will be waiting a while.