Enums are a data type in TypeScript that allows you to define a set of named constants. It can be numeric or string-based. Enums cannot be used as variables; doing so would return errors. Enums are type-safe when you plan to reassign or modify the enum member values and would return compile errors on reassignment.
Types of Enums
There are three types of enums:
- Numeric enum
- String enum
- Heterogeneous enum
Numeric Enum
To create a numeric enum in TypeScript, use the enum keyword.
npm install -g typescript
It will install globally on your machine.
You can compile using the following command.
tsc filename.ts
Write the following code inside the app.ts file.
enum score {
batsman,
bowler,
fielder,
Umpire,
}
Let’s transpile this file by the following command.
tsc app.ts
It will create the app.js file in the same directory.
The content of the app.js file is the following.
var score;
(function (score) {
score[score["batsman"] = 0] = "batsman";
score[score["bowler"] = 1] = "bowler";
score[score["fielder"] = 2] = "fielder";
score[score["Umpire"] = 3] = "Umpire";
})(score || (score = {}));
The JavaScript transpiler will emit the lookup table, which looks like the above table.
When you have score.batsman in TypeScript, it emits score.batsman in JavaScript, which evaluates to 0.
The score[0] emits score[0] and evaluates to “batsman”. The reverse lookup options are unique to enums and can be pretty convenient.
Enums are permanently assigned numeric values when they are stored. The first value always takes a numeric value of 0, while the other property values in the enum are incremented by 1.
We also have the option to initialize the first numeric value ourselves. For example, we can write the same enum as the following.
enum score {
batsman = 100,
bowler,
fielder,
Umpire,
}
Now, transpile to JavaScript.
tsc app.ts
It will create an app.js file.
var score;
(function (score) {
score[score["batsman"] = 100] = "batsman";
score[score["bowler"] = 101] = "bowler";
score[score["fielder"] = 102] = "fielder";
score[score["umpire"] = 103] = "Umpire";
})(score || (score = {}));
In this example, we have initialized the batsman to 100; from then, it will increment by 1 to the following properties.
That is why we get bowler = 101, fielder = 102, and umpire = 103.
Enum as a function parameter
The enum can be used as a function parameter. Let’s see how to pass an enum as a function parameter.
enum data {
x = 19,
y = 21
}
function respond(recipient: string, message: data): void {
console.log(recipient + message)
}
respond("The value of x is: ", data.x)
In this example, we have defined an enum called data.
Then we have defined a function called respond(), which takes two parameters.
- recipient – It is a type of string.
- message – It is a type of enum that we have recently defined.
Then in the body, we print both arguments and call the respond() method.
Using the enum is simple: access any member as the property off of an enum itself, and declare types using the enum’s name.
You will see the following output if you transpile to Javascript and run the JavaScript code.
The value of x is: 19
Computed Enums in TypeScript
Numeric enums can include members with a computed numeric value.
The value of the enum member can be either a constant or computed.
The following enum includes members with computed values.
enum Millie {
x = 19,
y = 21,
z = fetchValue('Eleven')
}
function fetchValue(val: String) {
if (val == 'Eleven') {
return 11
}
else {
return 21
}
}
console.log(Millie.z)
Transpile to Javascript code and run the file. You will see the following output.
11
But there is one caveat when we use computer enums.
Enums without initializers must be first or after the numeric enums initialized with numeric constants or other constant enum members.
In other words, when the enum includes computed and constant members, the uninitiated enum members must come first or after other initialized members with numeric constants.
If you do the following code, then you will get an error.
enum Millie {
x = 19,
z = fetchValue('Eleven'),
y
}
function fetchValue(val: String) {
if (val == 'Eleven') {
return 11
}
else {
return 21
}
}
console.log(Millie.z)
The above code shows that z has computed property, and next y is not initialized.
If you try to transpile the file, you will get the following error.
app.ts:4:3 – error TS1061: Enum member must have initializer.
The above enum can be declared as below to avoid the above error.
enum Millie {
x = 19,
y,
z = fetchValue('Eleven'),
}
Now, it will not give any error.
String Enum
String enums are similar to numeric ones, except the enum values are initialized with the string values rather than numeric ones.
The advantage of using string enum is that string enum offers better readability. If we have to debug code, it is easier to read string values than numeric ones.
If you print the variable’s value, you won’t get a mysterious 0 or 1; you will get the actual string itself every time.
Because of that, it’s also easily JSONifiable and produces the JSON you can read in other languages without maintaining the enum mapping.
enum StrangerThings {
Character = "Eleven",
Father = "Hopper",
Power = "Telekenesis",
Town = "Hawkins"
}
console.log(StrangerThings.Character)
console.log(StrangerThings.Power)
Output
Eleven
Telekenesis
In the above code, we have defined the string enum, StrangerThings, with the same values as the numeric enum above, but with a difference that these enum values are initialized with string literals.
The difference between numeric and string enum is that numeric enum values are auto-incremented, while string enum values must be individually initialized.
Let’s say if you don’t initialize the Father property, you will get an error like the following.
app.ts:3:3 – error TS1061: Enum member must have initializer. For example, try to transpile the following code.
enum StrangerThings {
Character = "Eleven",
Father,
Power = "Telekenesis",
Town = "Hawkins"
}
console.log(StrangerThings.Character)
console.log(StrangerThings.Power)
In this example, the Father is not initialized. So it will give us an error.
If you initialize it as an empty string, you won’t get any error.
enum StrangerThings {
Character = "Eleven",
Father = "",
Power = "Telekenesis",
Town = "Hawkins"
}
console.log(StrangerThings.Character)
console.log(StrangerThings.Father)
Output
Eleven
It will print the empty string.
If you were debugging the code and had to read the runtime value of the numeric enum, it is often opaque and doesn’t convey any useful meaning on its own (though reverse mapping can often help).
String enums enable us to give meaningful and readable value when your code runs, independent of an enum member’s name.
Heterogeneous Enum
Heterogeneous enums contain both string and numeric values.
enum StrangerThings {
Character = "Eleven",
Father = "",
Power = "Telekenesis",
age = 15
}
console.log(StrangerThings.Character)
console.log(StrangerThings.age)
Output
Eleven
15
Unless you’re trying to take advantage of JavaScript’s runtime behavior smartly, it’s advised that you don’t use a heterogeneous enum.
Reverse Mapping
TypeScript enum supports reverse mapping. In reverse mapping, we can access a member’s value and a member’s name from its value.
enum StrangerThings {
Character = 11,
Father,
Power,
age = 15
}
console.log(StrangerThings.Power)
console.log(StrangerThings["Power"])
console.log(StrangerThings[13])
Output
13
13
Power
From the above code, StrangerThings[13] returns its member name, “Power”. This is because of reverse mapping.
enum StrangerThings {
Character = 11,
Father,
Power,
age = 15
}
console.log(StrangerThings)
Output
{
'11': 'Character',
'12': 'Father',
'13': 'Power',
'15': 'age',
Character: 11,
Father: 12,
Power: 13,
age: 15
}
The output shows that each value of the enum appears twice in the internally stored enum object.
We know that enum values can be retrieved using a corresponding enum member value.
But it is also true that enum members can be retrieved using their values.
This is what we call reverse mapping.
Both mappings are true to the enum: name -> value and value -> name.
const enum
In most cases, enums are a supremely valid solution. However, sometimes requirements are more airtight. To avoid paying the cost of extra generated code when accessing the enum values, it’s possible to use the const enums.
The const enums are defined using a const modifier on our enums.
We can write the following code to define a const enum.
const enum Dark { Mikkel, Jonas }
console.log(Dark.Mikkel)
console.log(Dark['Mikkel'])
console.log(Dark.Jonas)
console.log(Dark['Jonas'])
Output
0
0
1
1
As usual, Enums are always assigned numeric values when they are stored.
The first value always takes a numeric value of 0, while the other values in the enum are incremented by 1.
But what about Dark[0]? This will error out at runtime, and your compiler should catch it. There is no lookup table, and the compiler doesn’t inline here.
const enum Data { Mikkel, Jonas }
console.log(Data[0])
console.log(Data[1])
Output
app.ts:3:18 - error TS2476: A const enum member can only be accessed using a string literal.
3 console.log(Dark[0])
~
app.ts:4:18 - error TS2476: A const enum member can only be accessed using a string literal.
4 console.log(Dark[1])
~
Found 2 errors.
Here, we got: A const enum member can only be accessed using a string literal.
In the above code, the –preserveConstEnumsflag will cause Dark to emit the lookup table. Its values will still be inlined, though.
Ambient enum
Ambient enums are used to describe the shape of already existing enum types.
declare enum Enum {
A = 11,
B,
C = 21
}
A critical difference between ambient and non-ambient enums is that, in regular enums, members that don’t have the initializer will be considered constant if their preceding enum member is deemed constant.
In contrast, an ambient (and non-const) enum member that does not have an initializer is always considered computed.
Enums can be used inside array initializations just as other TypeScript data types.
That’s it.