If we want to define constants in JavaScript, we can use the const
keyword. With TypeScript, we have another way to define a set of constants called enums. Enums let us define a list of named constants. It’s handy for defining an entity that can take on a few possible values. In this article, we’ll continue from Part 1 and look at union enums and enum member types, how enums are evaluated at run-time, const
enums, and ambient enums.
Union Enums and Enum Member Types
A subset of enum members can act as data types of variables and class members in TypeScript. We can use literal enum members, which are enum members with no specific values assigned to them for annotating data types of our variables and class members. If an enum member has no string literal, numeric literal or a numeric literal with a minus sign before it, then we can use them as data types for other members. For example, we can use them as we do in the following code:
enum Fruit {
Orange,
Apple,
Grape
}
interface OrangeInterface {
kind: Fruit.Orange;
color: string;
}
interface AppleInterface {
kind: Fruit.Apple;
color: string;
}
class Orange implements OrangeInterface {
kind: Fruit.Orange = Fruit.Orange;
color: string = 'orange';
}
class Apple implements AppleInterface{
kind: Fruit.Apple = Fruit.Apple;
color: string = 'red';
}
let orange: Orange = new Orange();
let Apple: Orange = new Apple();
In the code above, we used our Fruit
enum to annotate the type of the kind
field in our OrangeInterface
and AppleInterface
. We set it so that we can only assign Fruit.Orange
to the kind
field of the OrangeInterface
and the class Orange
which implements the OrangeInterface
. Likewise, we set the kind
field of AppleInterface
to the type Fruit.Apple
so that we can only assign the kind
field to the value of the instances of the Apple
class. This way, we can use the kind
field as a constant field even though we can’t use the const
keyword before a class field.
If we log the values of orange
and apple
above, we get that orange
is:
{kind: 0, color: "orange"}
and apple
has the value:
{kind: 1, color: "red"}
When we use enums in if
statements, the TypeScript compiler will check that if the enum members are used in a valid way. For example, it’ll prevent us from writing expressions that use enums that always evaluate to true
or false
. For example, if we write:
enum Fruit {
Orange,
Apple,
Grape
}
function f(x: Fruit) {
if (
x !== Fruit.Orange ||
x !== Fruit.Apple ||
x !== Fruit.Grape
) {
}
}
Then we get the error message “This condition will always return ‘true’ since the types ‘Fruit.Orange’ and ‘Fruit.Apple’ have no overlap.(2367)“ since at least one of them is always true
, so the expression:
x !== Fruit.Orange ||
x !== Fruit.Apple ||
x !== Fruit.Grape
will always evaluate to true
. This is because if x
can only be of type Fruit
, and if x
isn’t Fruit.Orange
, then it’s either Fruit.Apple
or Fruit.Grape
, so either of them must be true
.
This also means that the enum type itself is the union of each member, since each member can be used as a type. If a data type has the enum as the type, then it must always have one of the members in it as the actual type.
How Enums are Evaluated at Runtime
Enums are converted to real objects when they’re compiled by the TypeScript compiler, so they’re always treated like objects at runtime. This means that if we have an enum, then we can use its member names as property names of an enum object when we need to pass it in as a parameter with it. For example, if we have the following code:
enum Fruit {
Orange,
Apple,
Grape
}
function f(fruit: { Orange: number }) {
return fruit.Orange;
}
console.log(f(Fruit));
Then we get 0 from the console.log
output from the code in the last line since we logged the value of fruit.Orange
, which is 0 since we didn’t initialize it to any value. Likewise, we can use the same syntax for the destructuring assignment of an enum like we do in the following code:
enum Fruit {
Orange,
Apple,
Grape
}
let { Orange }: { Orange: number } = Fruit;
console.log(Orange);
In the code above, we treat the Orange
member inside the Fruit
enum as another property of an object, so we can use it to assign it to a new variable with destructuring assignment like we did above. So if we log Orange
like we did on the last line of the code snippet above, then we get 0 again. Also, we can use destructuring assignment to assign it to a variable with a name that’s not the same as the property name like we do in the following code:
let { Orange: orange }: { Orange: number } = Fruit;
console.log(orange);
Then we should get 0 again from the console.log
statement on the last line of the code above.
Photo by Gary Bendig on Unsplash
Enums at Compile Time
The only exception to the rule that enums are treated as objects if when we use the keyof
keyword with enums. The keyof
keyword doesn’t work like typical objects. For example if we have:
let fruit: keyof Fruit;
Then the TypeScript compiler expects that we assign strings with number methods to it. For example, if we try to assign something like a 'Orange'
to the expression above, we get the following error:
Type '"Orange"' is not assignable to type '"toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"'.(2322)
This isn’t what expect from the typical usage of the keyof
keyword since for normal objects, it’s supposed to let us assign the property names of the keys of an object that comes after the keyof
keyword. To make TypeScript let us assign 'Orange'
, 'Apple'
or 'Grape'
to it, we can use the typof
keyword after the keyof
keyword like we do in the following code:
enum Fruit {
Orange,
Apple,
Grape
}
let fruit: keyof typeof Fruit = 'Orange';
The code above would be accepted by the TypeScript compiler and runs because this is what makes TypeScript treats our enum members’ names as key names of an object.
Reverse Mappings
Numeric enums in TypeScript can be mapped from enum values to enum names. We can get an enum member’s name by its value by getting it by the values that are assigned to it. For example, if we have the following enum:
enum Fruit {
Orange,
Apple,
Grape
}
Then we get can the string 'Orange'
by getting it by its index like we do with the following code:
console.log(Fruit[0]);
The code above should log 'Orange'
since the value of the member Orange
is 0 by since we didn’t assign any specific value to it. We can also access it by using the member constant inside the brackets like the following code:
console.log(Fruit[Fruit.Orange]);
Since Fruit.Orange
has the value 0, they’re equivalent.
Const Enums
We can add the const
keyword before the enum definition to prevent it from being included in the compiled code that’s generated by the TypeScript compiler. This is possible since enums are just JavaScript objects after it’s compiled. For this reason, the values of the enum members can’t be dynamically generated, but they can be computed from other constant values. For example, we can write the following code:
const enum Fruit {
Orange,
Apple,
Grape = Apple + 1
}
let fruits = [
Fruit.Orange,
Fruit.Apple,
Fruit.Grape
]
Then when our code is compiled into ES5, we get:
"use strict";
let fruits = [
0 /* Orange */,
1 /* Apple */,
2 /* Grape */
];
Ambient Enums
To reference an enum that exists somewhere else in the code, we can use the declare
keyword before the enum definition to denote that. Ambient enums can’t have values assigned to any members and they won’t be included in compiled code since they’re supposed to reference enums that are defined somewhere else. For example, we can write:
declare enum Fruit {
Orange,
Apple,
Grape
}
If we try to reference an ambient enum that’s not defined anywhere, we’ll get a run-time error since no lookup object is included in the compiled code.
Enum members can act as data types for variables, class members, and any other things that can be typed with TypeScript. An enum itself can also be a data type for these things. Therefore, anything typed with the enum type is a union type of all the member enum types. Enums are included or not depending on what keywords we use before the enum. If they’re defined with const
or declare
, then they won’t be included in the compiled code. Enums are just objects when converted to JavaScript and the members are converted to properties when compiled to JavaScript. This means that we can use member names as property names of objects in TypeScript.