TypeScript: Suggestion for enums
Not long ago I updated Angular to v16.0.0 and there was a bug in the Angular CLI when optimizing a CommonJS exported enum, the build optimizer enum wrapping pass was previously dropping the exports
object assignment from the enum wrapper function call expression. This would not occur with application code but is possible with library code that was built with TypeScript and shipped as CommonJS.
Assuming the following TypeScript enum:
export enum MyEnum{
A = 0,
B = 1,
}
TypeScript 5.1 will emit an exported enum for CommonJS as follows:
exports.MyEnum = void 0;
var MyEnum;
(function (MyEnum) {
MyEnum[MyEnum["A"] = 0] = "A";
MyEnum[MyEnum["B"] = 1] = "B";
})(MyEnum || (exports.MyEnum = MyEnum = {}));
The build optimizer would previously transform this into:
exports.MyEnum = void 0;
var MyEnum = /*#__PURE__*/ (() => {
MyEnum = MyEnum || {};
MyEnum[(MyEnum["A"] = 0)] = "A";
MyEnum[(MyEnum["B"] = 1)] = "B";
return MyEnum;
})();
But this has a defect wherein the exports
assignment is dropped. This behavior has been rectified since, and the build optimizer will now transform the code into:
exports.MyEnum = void 0;
var MyEnum = /*#__PURE__*/ (function (MyEnum) {
MyEnum[(MyEnum["A"] = 0)] = "A";
MyEnum[(MyEnum["B"] = 1)] = "B";
return MyEnum;
})(MyEnum || (exports.MyEnum = MyEnum = {}))
Nevertheless, it caused a bug for us as we didn’t have that fix on the version 16.0.0, so our code worked in dev, but once the optimization was enabled it didn’t work anymore.
This raised the question, of whether should we stop using enums altogether, the generated code is an IIFE which could be hard to optimize and could generate some unexpected behavior and we’ve had the experience of how could that happen firsthand. It’s not just a theoretical danger anymore it’s an experienced one.
But enums are useful, we need a way to accept only certain values, at build time without affecting the runtime. This is what we came up with:
/**
* A way to get the best of both worlds
* enabling easy refactoring while keeping structural typing
*/
type Enumerate<T extends Readonly<Record<string, string>>> = T[keyof T];
const MyEnum = {
DEBUG: 'debug',
INFO: 'info',
} as const;
type MyEnum = Enumerate<typeof MyEnum>;
function testFn(val: MyEnum): void {
console.log(val);
}
testFn(MyEnum.INFO);
testFn('info');
With this utility type, we get to keep the enum typing, the function testFn still accepts only certain values at build time, and the runtime code is no different from the code I am writing, my enum is now a predictable frozen object. We get the best of both worlds!
Thank you for reading.
In Plain English 🚀
Thank you for being a part of the In Plain English community! Before you go:
- Be sure to clap and follow the writer ️👏️️
- Follow us: X | LinkedIn | YouTube | Discord | Newsletter
- Visit our other platforms: Stackademic | CoFeed | Venture | Cubed
- More content at PlainEnglish.io