Working with Enumerations (Enums) in TypeScript

In this article, we are going to talk about the Enum data structure and its use cases. TypeScript provides an easy to use API to work with enums, both as a type and as a value.

TypeScript provides the enum keyword to define a set of labeled values. This can be a set of string or number values. However, it is recommended not to create an enum of mixed values (of these types). Let’s see a small example.

In the above example, we have defined the enum Speed that contains SLOW, MEDIUM and FAST members. We have set the value of SLOW to 1 and subsequent members get the incremented value of the previous member implicitly. If you do not set the value of the first member, its implicit value will be 0 and later members will be incremented in a similar fashion.

You are free to set custom numeric values to each member or only some of them. Those members whose value is not explicitly provided will be auto-incremented by looking at the previous member value. Enums in TypeScript isn’t only a compile-time feature. The enum type does actually gets compiled into a JavaScript object.

This program when compiled produces the following output.

// define an enum
var Speed;
(function (Speed) {
  Speed[(Speed["SLOW"] = 0)] = "SLOW";
  Speed[(Speed["MEDIUM"] = 1)] = "MEDIUM";
  Speed[(Speed["FAST"] = 2)] = "FAST";
})(Speed || (Speed = {}));

console.log(Speed);

As you can see from the above example, Speed is actually a variable that we can reference in JavaScript and it is an object at runtime. We have member names (labels) as keys in this object with their respective values in the enum.

We also get a reverse mapping of keys and values in this object. This reverse mapping is useful when you want to access the member name (label) of the enum using a value. This is explained in the following example.

The most preferred way to use enums is with string values. Though string enums do not have the feature of auto-incrementing values (since string value can not increment), they provide good visual aid while debugging since their member values are more verbose than the integer.

In the above example, we have Speed enum but with string values. In the case of string members, the output enum object does not have reverse mapping as you can see from the result.

Constant Enums

So far we have seen that enums get injected into the compiled JavaScript code as plain JavaScript objects. Wherever we use the enum member reference in the source code, that reference will point to this generated object in the output code. Let’s take a simple example.

In the above example, we have the Speed enum with some string values. The racer object’s speed property has the value of Speed.MEDIUM member. If we compile this code, the JavaScript output will look like this.

// enum-non-const.js

// define a non-constant enum
var Speed;
(function (Speed) {
  Speed["SLOW"] = "slow";
  Speed["MEDIUM"] = "medium";
  Speed["FAST"] = "fast";
})(Speed || (Speed = {}));

// define a simple object
var racer = {
  name: "Ross Geller",
  speed: Speed.MEDIUM,
};

As you can see, the JavaScript output contains the Speed object and speed property of the racer object points to the MEDIUM value of this object.

This sometimes adds unnecessarily overhead if you aren’t necessarily doing anything with this object. In some cases, instead of having reference to the output object, you want the enum member value to be inline.

If we put const keyword before the enum declaration, the TypeScript compiler will substitute the value of the member reference in place.

In the above example, we have the same enum Speed we used before but now it’s a constant enum since we added the const keyword before it. This time, the speed property will get the 'medium' literal value instead of Speed.MEDIUM expression in the compiled JavaScript code. Also, there won’t be a Speed object in the output code.

// enum-const.js
// define a simple object
var racer = {
  name: "Ross Geller",
  speed: "medium" /* MEDIUM */,
};

// log `racer` object
console.log("racer =>", racer);

💡 TypeScript adds a JavaScript comment containing the name of the enum member where the enum member reference was substituted in the code. For example, you can see the /* MEDIUM */ comment in the above code.

Enum Type and Enum Member Type

When we define an enum, TypeScript also defines it as a type with the same name (just like class definition). A variable (entity) annotated with this type must reference the member of this enum.

In the above example, the Racer interface has the speed property of the type Speed which is an enum. Hence any value of the type Racer must have speed property and is value must be one of the members of the Speed. In the above example, the value of the ross.speed property is Speed.MEDIUM.

Since enum members have constant values of string or number, therefore it is legal to provide a literal value for an entity of type enum. For example, ross.speed = 3 is legal since is equivalent to Speed.FAST. Also, the reverse is also true as shown below.

let fastValue: number = Speed.FAST; // legal

Enum members themselves are also types on their own. You can apply the analogy of literal types to this. The enum type is a collective type while enum members also have their own unit types.

💡 The literal types concept is explained in the Type System lesson.

In the above example, the Racer interface has the speed property of the type Speed.MEDIUM, which means the value of this property can only be Speed.MEDIUM value and nothing else.

💡 The same rules apply for the constant enums.

Computed Enum Members

In most of the programming languages, the enum members must have compile-time constant values which mean all the values must be defined during compilation. But TypeScript allows expressions as values for the enum members which are computed at runtime.

TypeScript divides enum members based on their value initialization. If the value is available in the compilation phase, they are called constant members and when the value will be evaluated at runtime, they are called computed members. Let’s see a small example of this.

In the above example, Speed enum has SLOW and MEDIUM members whose values can be predicated at compile-time, hence they are constant members. However, the parseInt function is only available at runtime, hence the FAST member is a computed member.

If an enum member doesn’t have a value initializer, then its previous member must be a numeric constant member so that the TypeScript compiler can assign an incremented value during the compile time.

💡 Constant enums (using const) can not have computed members since their values must be present during compilation phase.

TypeScript allows enum members to reference values from the same enum or another enum. You are also allowed to use +, -, ~ unary operators as well as +, -, *, /, %, <<, >>, >>>, &, |, ^ binary operators in the member value initialization expression which can only be used with constant values or constant enum members from the same or another enum.

In the example above, the MEDIUM and FAST members of the Speed enum are computed using the members of the same enum. Using the OR binary operator (|), we can achieve some pretty useful enums.

#typescript #basics #enums