Why are Typescript Enums harmful?

What are Enums in TS
Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript.
Enums allow a developer to define a set of named constants. Using Enums can make it easier to document intent, or create a set of distinct cases. TypeScript provides both numeric and string-based Enums.
enum Direction {
Up, //0
Down, //1
Left, //2
Right, //3
}
enum Direction {
Up = 1, //1
Down, //2
Left, //3
Right, //4
}
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
// enums without initializers either need to be first,
// or have to come after numeric enums initialized with
// numeric constants or other constant enum members
enum E {
B,
A = getSomeValue(),
}
enum MixedEnum {
No = 0,
Yes = "YES",
}
const enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
Agenda
- Enums are not removed from the production bundle.
- Enums behave unpredictably at runtime.
- Enums make TypeScript a nominal type system.
- Cannot exchange enums that have same keys and values.
- Enums Teleport between JS world and Typescript world.
- Cannot mark enums imports with `type`
keyword
. - Using `const enums` can be risky.
- New flag `erasableSyntaxOnly`
- What is the replacement for enums
They are not removed from the production bundle
Enums are emitted with the production code which can increase the production bundle size if you have many large enums while other types are removed during build time.
type T = number;
type T2 = string | number;
interface I {
key:"some text";
key2: boolean;
key3: Record<PropertyKey, number>;
}
const foo:T = 5;
const foo2: T2 = "really long text";
const foo3: I = {
key: "some text",
key2: true,
key3: {
nestedKey: 5
}
}
"use strict";
const foo = 5;
const foo2 = "really long text";
const foo3 = {
key: "some text",
key2: true,
key3: {
nestedKey: 5
}
};
They are not removed from the production bundle
enum E {
DEBUG = 1,
WARNNING = 2
}
enum E2 {
DEBUG = "debug",
WARNNING = "warning"
}
const foo: Record<PropertyKey,E> = {
debug: E.DEBUG,
warning: E.WARNNING
}
const foo2: Record<PropertyKey,E2>= {
debug: E2.DEBUG,
warning: E2.WARNNING
}
"use strict";
var E;
(function (E) {
E[E["DEBUG"] = 1] = "DEBUG";
E[E["WARNNING"] = 2] = "WARNNING";
})(E || (E = {}));
var E2;
(function (E2) {
E2["DEBUG"] = "debug";
E2["WARNNING"] = "warning";
})(E2 || (E2 = {}));
const foo = {
debug: E.DEBUG,
warning: E.WARNNING
};
const foo2 = {
debug: E2.DEBUG,
warning: E2.WARNNING
};
They behave unpredictably at runtime
enum E {
DEBUG = 1,
WARNNING = 2
}
enum E2 {
DEBUG = "debug",
WARNNING = "warning"
}
const EAsObj = {
"1": "DEBUG",
"2": "WARNNING",
"DEBUG": 1,
"WARNNING":2
}
const E2AsObj = {
"DEBUG": "debug",
"WARNNING": "warning"
}
When attempting to enumerate their keys or values, you would expect to receive only the keys or values. However, the result is inconsistent and not as anticipated.
TypeScript is a structural type system, meaning it focuses on the runtime values rather than their names. However, with enums, you cannot replace `E.DEBUG` with `"DEBUG"`, which introduces a nominal aspect to TypeScript, where the names of things do matter.
They make TypeScript a nominal type system
enum E {
DEBUG = "DEBUG",
WARNNING = "WARNNING"
}
const foo = (value:E)=>{
console.log(value)
}
foo(E.DEBUG)
//Argument of type '"DEBUG"' is not assignable
//to parameter of type 'E'.(2345)
foo("DEBUG")
type TObj = {
"DEBUG": "DEBUG";
"WARNNING": "WARNNING"
}
const obj: TObj ={
"DEBUG": "DEBUG",
"WARNNING": "WARNNING"
}
const foo2 = (value: TObj[keyof TObj])=>{
console.log(value)
}
foo2("DEBUG")
foo2(obj.DEBUG)
Cannot exchange enums that have same keys and values
enum E {
DEBUG = "debug",
WARNNING = "warning"
}
enum E2 {
DEBUG = "debug",
WARNNING = "warning"
}
const foo = (value:E)=>{
console.log(value)
}
foo(E.DEBUG)
// Argument of type 'E2.DEBUG' is not assignable
// to parameter of type 'E'.(2345)
foo(E2.DEBUG)
They Teleport between JS world and TS world
enum E {
DEBUG = "DEBUG",
WARNNING = "WARNNING"
}
// Type '{ DEBUG: string; WARNNING: string; }'
// is not assignable to type 'E'.(2322)
const bar: E = {
DEBUG: "DEBUG",
WARNNING: "WARNNING"
};
const foo = (values: E,key: E.DEBUG|E.WARNNING)=>{
// Element implicitly has an 'any' type because
// index expression is not of type 'number'.(7015)
console.log(values[key])
}
foo(bar,E.DEBUG);
// Argument of type 'typeof E' is
// not assignable to parameter of type 'E'.(2345)
foo(E,E.DEBUG);
Although enums are compiled as plain old JavaScript objects (POJOs), they can be used to type function parameters, but they cannot be passed as arguments.
You cannot mark enums imports with `type` keyword
// "verbatimModuleSyntax"
import type { Foo } from "./SomeModule"
const test = (value: Foo): void => {
console.log(value)
}
Type imports should be marked with the `type` keyword to exclude them from the production bundle. However, enum imports cannot be marked as types if they are used as values for comparison or for assignment.
import type { E1 } from "./SomeModule"
const test = (value: E1): void => {
//'E1' cannot be used as a value because
//it was imported using 'import type'.ts(1361)
if (value === E1.value) console.log(value)
}
`const enums` can be risky
using `const enum` can solve most of the issues we talked about, but according to TS docs, there is a long section to illustrate the pitfalls of using them because:
- Because they are just replaced with the actual values at build time, using them with a compiler that you do not control can be risky and can introduce unexpected bugs, especially inside library code.
- If ambient const enums are used inside library code, consumers will not be able to use `
isolatedModules`.
- If used inside a library, code consumers could use a version A of the library at compile time and a version B of the same library at runtime with the same enum but with different values, which could cause unexpected bugs.
- They still cannot be imported with `type` keyword
`const enums` can be risky
const enum E {
DEBUG = 1,
WARNNING = 2
}
const enum E2 {
DEBUG = "DEBUG",
WARNNING = "WARNNING"
};
const foo = (values: E)=>{
console.log(values === E.DEBUG)
}
const foo2 = (values: E2)=>{
console.log(values === E2.DEBUG)
}
"use strict";
const foo = (values) => {
console.log(values === 1 /* E.DEBUG */);
};
const foo2 = (values) => {
console.log(values === "DEBUG" /* E2.DEBUG */);
};
`erasableSyntaxOnly` Flag
it got released with V5.8. It tells the TypeScript compiler to erase certain syntax constructs that are only relevant during type checking and not needed in the emitted JavaScript.
// ❌ error: An `import ... = require(...)` alias
import foo = require("foo");
// ❌ error: A namespace with runtime code.
namespace container {
foo.method();
export type Bar = string;
}
// ❌ error: An `import =` alias
import Bar = container.Bar;
class Point {
// ❌ error: Parameter properties
constructor(public x: number, public y: number) {}
}
// ❌ error: An `export =` assignment.
export = Point;
// ❌ error: An enum declaration.
enum Direction {
Up,
Down,
Left,
Right,
}
What is the replacement for enums
There are many options to replace enums with but the best option and the most famous is using (POJOs) plain old js objects marked with `as const`
, so `as const`
do two things:
- it makes the object properties read-only means they cannot be manipulated or changed.
- it narrows the type of that object to be the exact values of the object
What is the replacement for enums
const obj = {
DEBUG: "DEBUG",
WARNING: "WARNING"
} as const
type ObjValue = typeof obj[keyof typeof obj]
const foo = (value: ObjValue)=>{
console.log(value)
}
foo("DEBUG");
foo(obj.DEBUG);
then we can extract the values of that object as types using some typescript magic.
This approach eliminates the need to import `obj
` every time you want to use it, hence you can only pass the value
What is the replacement for enums
Or, we can extract the type from the object keys and we can map these keys to any other human readable values we want.
const obj = {
DEBUG: "this is a debug message",
WARNING: "this is awarining message",
} as const
type ObjKey = keyof typeof obj
const foo = (key: ObjKey)=>{
console.log(obj[key])
}
foo("DEBUG")
Todo the same as the above with enums you will have to create the enum and create an object that have enum values as keys and map them to actual values which alot more code
enum E {
DEBUG = "DEBUG",
WARNING = "WARNING",
}
const obj = {
[E.DEBUG]: "debug message",
[E.WARNING]: "warning message"
}
const foo = (key: E)=>{
console.log(obj[key])
}
foo(E.WARNING)
Any Questions ?
Why are typescript enums harmful?
By ahmed saeed
Why are typescript enums harmful?
- 28