Core Concepts
Enumerations are a common feature in programming languages, used to represent a fixed collection of distinct values.
Discrete Values
When we refer to discrete values, we are talking about values that are separate and not part of a continuous range. This concept is central to a field known as Discrete Mathematics, which focuses on such values.
A definition from Wikipedia:
Discrete mathematics is the study of mathematical structures that are fundamentally discrete rather than continuous. In contrast to real numbers that have the property of varying "smoothly", the objects studied in discrete mathematics — such as integers, graphs, and statements in logic— do not vary smoothly in this way, but have distinct, separated values.
To illustrate:
- Integers, booleans, and cardinal numbers fall into the discrete category.
- Real numbers, on the other hand, are continuous.
A useful way to grasp this is that between any two continuous values, you can always find another one. This is not the case for discrete values; for instance, between the integers 1 and 2, there is no other integer.
Cardinality
Cardinality is simply a term for the size of a set. As an example, the set of months in a year has a cardinality of 12.
It's important to note that discrete mathematics doesn't require a set to have a finite cardinality. For instance, the set of all integers contains an infinite number of elements.
In practice, however, the enumerations we use in programming tend to have a relatively small cardinality.
Immutability
An object is considered immutable if it cannot be changed after it is created. Sets of objects are often mutable, allowing new elements to be added. For enumerations, we generally want them to be immutable to prevent accidental modifications that could introduce bugs into our code.
Enumerations in JavaScript
JavaScript itself doesn't provide a native enumerated type. However, it's straightforward to create objects that emulate enumeration behavior:
const Color = {
red: 0,
blue: 1,
yellow: 2,
};
This pattern works quite well, provided we are careful in how we use it:
console.log ('We can use these like this:');
console.log ('Color.red:', Color.red);
// Color.red: 0
console.log ('We get undefined when we use it incorrectly');
console.log ('Color.blew:', Color.blew);
// Color.blew: undefined
If our code were to encounter an undefined value, it would likely fail at run-time. This failure would be a clear signal that we didn't use our enumeration correctly.
Mutability Issue
The main drawback here is that our Color object is still mutable. This means there is a risk of unintentionally changing the object:
Color.yellow = 5;
console.log ('Color.yellow:', Color.yellow);
// Color.yellow: 5
To prevent this, we can leverage the Object.freeze function. For instance:
const Compass = {
north: 0,
east: 1,
south: 2,
west: 3
};
Object.freeze(Compass);
console.log ('Compass.north:', Compass.north);
// Compass.north: 0
// Now we try to modify the Compass object
Compass.north = 5;
console.log ('Compass.north:', Compass.north);
// Compass.north: 0
The Object.isFrozen function can be used to check whether an object has been frozen with Object.freeze. Here’s an example:
if (Object.isFrozen(Compass)){
console.log ('Compass is immutable.');
} else {
console.log ('Compass is mutable.');
}
Integers Versus Strings
So far, we've assumed that a back-end system or function requires an integer input. If you have a choice in the matter, defining your enumeration with strings is often more advantageous.
const Compass = {
north: 'north',
east: 'east',
south: 'south',
west: 'west'
};
Object.freeze(Compass);
The benefit of this approach becomes clear when you need to log the value or store it in a database. For example, if we console.log our value, we'd see:
console.log ('Compass.north:', Compass.north);
// Compass.north: north
TypeScript Enums
TypeScript provides native support for enumerations via the enum keyword.
You can find the official documentation here: https://www.typescriptlang.org/docs/handbook/enums.html
In TypeScript, an enum is defined as follows:
enum Compass {
North,
East,
South,
West
}
We can use it in a manner similar to our JavaScript object approach:
let myDirection: Compass = Compass.North;
console.log('myDirection', myDirection);
// myDirection: 0
The key difference is TypeScript's build process. For example, if we attempt to use a non-existent enum value:
// typo Sowth instead of South
console.log('Compass.Sowth', Compass.Sowth);
We will immediately get a build-time error:
$ tsc tsenum.ts
tsenum.ts:10:38 - error TS2551: Property 'Sowth' does not exist on type 'typeof Compass'. Did you mean 'South'?
10 console.log('Compass.Sowth', Compass.Sowth);
~~~~~
tsenum.ts:4:5
4 South,
~~~~~
'South' is declared here.
Found 1 error.
It's noteworthy that TypeScript can even anticipate our intention and offer a helpful suggestion.
Immutability
TypeScript enums are also immutable. For instance, if we try:
// Try to add a new value to the enum
Compass.SouthEast = 7;
// Try to modify an existing value
Compass.South = 7;
This will, as expected, result in a build-time error:
$ tsc tsenum-errors.ts
tsenum-errors.ts:14:9 - error TS2339: Property 'SouthEast' does not exist on type 'typeof Compass'.
14 Compass.SouthEast = 7;
~~~~~~~~~
tsenum-errors.ts:15:9 - error TS2540: Cannot assign to 'South' because it is a read-only property.
15 Compass.South = 7;
~~~~~
Found 2 errors.
Converting to Strings
Using array notation, we can easily convert our enum values to strings:
// We can even get the string value
const directionName: string = Compass[Compass.South];
console.log('directionName', directionName);
// directionName South
This technique works because the numeric value of the enum corresponds directly to the index of the item. This approach is even compatible with enums that don't start at zero. For example, you can set the starting number to 2:
enum Compass {
North = 2,
East,
South,
West
}
// Yes, this still works
const directionName: string = Compass[Compass.South];
console.log('directionName', directionName);
// directionName South
Constant Enums
If your enum contains only constant values, it's highly recommended to make it a constant enum.
const enum Compass {
North,
East,
South,
West
}
Adding the const keyword means the enum will be completely removed during the compilation step. References to the const enum are inlined at the point of use, meaning the actual values are substituted directly into the generated code.
For instance, with this TypeScript code:
const foo: Compass = Compass.North;
The compiled JavaScript output is:
var foo = 0;
In most cases, constant enums offer all the advantages of regular enumerations without any runtime overhead. However, as pointed out by Russ Painter, there is one notable exception: the string conversion trick won't work. For example, this attempt will not succeed with a constant enum:
const directionName: string = Compass[Compass.South];
Diving Deeper
The topic of enums goes beyond this brief overview. TypeScript offers several other variations, including String enums, Heterogeneous enums, Union enums, and Ambient enums.
To learn more about these specific types, the official TypeScript enum documentation is an excellent resource:
https://www.typescriptlang.org/docs/handbook/enums.html
Enumerated types are a feature in many different programming languages. As we've seen, a solid grasp of the general concept is valuable, regardless of the language:
https://en.wikipedia.org/wiki/Enumerated_type
For a more academic perspective on how enumerations relate to other data types, you can explore the field of Type Theory:
https://en.wikipedia.org/wiki/Type_theory
