Prelude
This article is not intended to teach you how to use enums in Typescript, but rather to discuss the problems of using them in real-world scenarios, based on our experiences and some references.
Note that link marks a hyperlink to TypeScript Playground for checking out examples.
Introducing Enums
Enums are provided by TypeScript to define constants with names that clearly express intent, or to create a set of distinguished cases.
The value and type of an enum are one and the same, and the type of an enum member is a subtype of that enum type. There are two scenarios to be aware of when defining enums.
- When all enum members are literal enum values, all of these members are both values and types.
- If there are non-literal members in the enum, then all members of that enum can only be used as values.
/* enum members are all literal values */
enum Day {
Monday,
Tuesday,
Wednesday,
}
const monday: Day = Day.Monday; // valid 😎
const thusday: Day.Tuesday = Day.Tuesday; // valid 😎
/* enum members contain non-literal enum values */
enum NextDay {
Monday,
Tuesday,
Wednesday = Day.Wednesday /* non-literal enum values */,
}
const nextMonday: NextDay = NextDay.Monday; // valid 😎
const nextThusday: NextDay.Tuesday = NextDay.Tuesday; // invalid 😓
const nextWednesday: NextDay.Wednesday = NextDay.Wednesday; // invalid 😓
You can see that Day and NextDay are compiled into JavaScript with exactly the same structure (link).
var Day;
(function (Day) {
Day[(Day["Monday"] = 0)] = "Monday";
Day[(Day["Tuesday"] = 1)] = "Tuesday";
Day[(Day["Wednesday"] = 2)] = "Wednesday";
})(Day || (Day = {}));
var NextDay;
(function (NextDay) {
NextDay[(NextDay["Monday"] = 0)] = "Monday";
NextDay[(NextDay["Tuesday"] = 1)] = "Tuesday";
NextDay[(NextDay["Wednesday"] = 2)] = "Wednesday";
})(NextDay || (NextDay = {}));
Fundamentals of Enums
Firstly in JavaScript, we generally tend to use objects for defining constant configurations. But in TypeScript, defining these constants with enums can be more terse and expressive.
/* JS: Constants Config */
const SERVICE_STATUS = {
SUCCESS: 200,
NOT_FOUND: 404,
UNKONW_ERROR: 500,
};
/* TS: Enum Config */
enum SERVICE_STATUS {
SUCCESS = 200,
NOT_FOUND = 404,
UNKONW_ERROR = 500,
}
Then, different definitions of enums leads to different behaviors and different compiled JavaScript output.
For example, here is an enum definition (link).
enum Day {
Monday = "monday",
Tuesday = "tuesday",
Wednesday = "wednesday",
}
const day: Day = Day.Monday;
Its corresponding JavaScript compiled output looks like this.
var Day;
(function (Day) {
Day["Monday"] = "monday";
Day["Tuesday"] = "tuesday";
Day["Wednesday"] = "wednesday";
})(Day || (Day = {}));
const day = Day.Monday;
So we can see in JavaScript, enums are converted to objects, and two-way mappings are added inside the objects, which undoubtedly increases size of the bundle.
As a comparison, we can take a look at the result of using theconst enum definition (link).
const enum Day {
Monday = "monday",
Tuesday = "tuesday",
Wednesday = "wednesday",
}
const day: Day = Day.Monday;
When the above code is compiled into JavaScript in strict mode, the constant enum definition is removed and its members are replaced with corresponding inlined values.
"use strict";
const day = "monday"; /* Day.Monday */
So it’s clear that enums defined by const enum in TypeScript are more likely to be used as values (inferred from the compiled JavaScript results). Whereas enums defined with enum can be used as both values and types.
Last thing to note is that two enums cannot be assigned to each other, even if their members are identical (link).
enum Day {
Monday,
Tuesday,
Wednesday,
}
enum NextDay {
Monday,
Tuesday,
Wednesday,
}
const day: Day = Day.Monday; // valid 😎
const nextMonday: Day.Monday = NextDay.Monday; // invalid 😭
const nextTuesday: NextDay.Tuesday = NextDay.Tuesday; // valid 😎
Caveats
Using enums as types can lead to some confusing behavior.
Firstly, in the world of structural typing, enums sticks to nominal typing. This means that even if one value is valid and compatible, it can’t be passed to a function or object that requires a string enum (link).
enum Direction {
Up = "up",
Down = "down",
Left = "left",
Right = "right",
}
declare function logDirection(direction: Direction): void;
logDirection("up"); // invalid 🤔
logDirection(Direction.Up); // valid
What if we replace it with a JavaScript constant object (link)?
const Direction = {
Up: "up",
Down: "down",
Left: "left",
Right: "right",
} as const;
type ValueOf<T> = T[keyof T];
declare function logDirection(direction: ValueOf<typeof Direction>): void;
logDirection("up"); // valid 😄
logDirection(Direction.Up); // valid
If the value of one enum member is a numeric literal, then the type of that enum will be widen to number (link).
enum SERVICE_STATUS {
SUCCESS = 200,
NOT_FOUND = 404,
UNKONW_ERROR = 500,
}
const getCode = (code: SERVICE_STATUS) => code;
getCode(200); // valid
getCode(123); // valid 😲
What if we replace it with a JavaScript constant object (link)?
const SERVICE_STATUS = {
SUCCESS: 200,
NOT_FOUND: 404,
UNKONW_ERROR: 500,
} as const;
type ValueOf<T> = T[keyof T];
const getCode = (code: ValueOf<typeof SERVICE_STATUS>) => code;
getCode(200); // valid
getCode(123); // invalid 😄
Heterogeneous enums (enum whose member types are different) can lead to weird behavior (link).
enum Direction {
Up,
Down,
Left,
Right = "right",
}
const logDirection = (direction: Direction) => direction;
logDirection(Direction.Up); // valid
logDirection(Direction.Right); // valid
logDirection("right"); // invalid 😮
logDirection(100); // valid 😱
What if we replace it with a JavaScript constant object (link)?
const Direction = {
Up: 0,
Down: 1,
Left: 2,
Right: "right",
} as const;
type ValueOf<T> = T[keyof T];
const logDirection = (direction: ValueOf<typeof Direction>) => direction;
logDirection(Direction.Up); // valid
logDirection(Direction.Right); // valid
logDirection("right"); // valid 😎
logDirection(100); // invalid 😱
Then, enums used in TypeScript can’t be tree shaked when compiled into JavaScript, because it’s compiled into IIFE.
To be clear, enums can be used to define some constants with names. But if the constants are required to be objects (e.g. complex configuration items), enums are obviously not sufficient to handle them (link).
/* sorting rules */
const Collation = {
ASC: {
key: "ASC",
value: 1,
},
DESC: {
key: "DESC",
value: 2,
},
} as const;
/* define types */
type ValueOf<T> = T[keyof T];
/* use it as configurations */
type Collation = ValueOf<typeof Collation>;
const collationOptions: Collation[] = Object.values(Collation);
/* use it as default values */
type CollationValue = ValueOf<typeof Collation>["value"];
const currentValue: CollationValue = Collation.ASC.value;
Takeaways
TypeScript has many advantages over JavaScript:
- Interface-oriented development brings great extensibility.
- Static type checking can help developers write more robust code, significantly improving code quality and comprehensibility. Types are one of the best forms of documentation.
- Code integrity and intelligent awareness, which can be one of the biggest advantages. TypeScript enums can clearly define simple configuration, its existence is reasonable, but not necessarily the most appropriate. The overall optimal solution is not always also the local optimal solution.
Based on the above analysis, we also see many defects (or designed features) of TypeScript enums, even when the source code is not compiled into JavaScript. In practice, then, it is possible to choose the best - using constant objects instead of enums when necessary. What we propose can make it possible to:
- Making types more rigorous and reliable.
- Allowing complex configuration items to complete the loop in daily use.