Enumerated Types in TypeScript
Enumerated Types, also known as enums, are a feature common to many programming languages, that represent a finite set of possible values for a given type.
For example, in a typical 52 card deck of cards, there is four suits: Clubs, Diamonds, Hearts and Spades. All of the 52 cards are one of these suites.
In a typed programming language, we could have a “Card” type, with a “suit” property. In the most basic form, the type could be defined where the value of “suit” must be a string. We could then assign the value “Clubs”, “Diamonds”, “Hearts”, or “Spades” for each card.
Here’s how that might be represented in TypeScript, and for simplicity, we’re going to make every card have a number value for now and not worry so much about face cards.
interface Card {
value: number // 2, 3, 4, etc...
suit: string // either "Clubs", "Diamonds", "Hearts", or "Spades"
}
With this setup, we could assign suit to be “Diamonds”, “Hearts”, etc, but we could also assign it any other string, like “Squares” or “Harts”, and the compiler would not complain, because we said the value can be any string, and “Squares” is a string.
It would be nice if the computer could help us avoid errors like these. That’s one of the things enums can do, along with documenting what the valid values are.
So to lock things down a bit tighter, we can define an enum for Suit, with all possible expected values defined. Here’s how that looks in TypeScript:
enum Suit {
Clubs,
Diamonds,
Hearts,
Spades,
}
Unless you say otherwise, the value of each item in an Enum is numeric, and starts at 0. Clubs == 0, Diamonds == 1, etc. It can start at a different number, or you can specifically set the value of each item.
You can also make the values be strings, which is handy if you’re dealing with something like a REST API returning JSON data where the value of suit is a string, not a number.
// GET https://awesome.api/cards/32
// JSON Response:
{
"value": 5,
"suit": "Diamonds"
}
Here’s a string enum for Suit:
enum Suit {
Clubs = "Clubs",
Diamonds = "Diamonds",
Hearts = "Hearts",
Spades = "Spades",
}
Slightly redundant since we have to repeat each word twice, but it’s more convenient, and the values map better to the data.
Now you can define a card interface with the more exact Suit enum value defined.
interface Card {
value: number
suit: Suit
}
Then we can define a card:
const card1: Card = {
value: 5,
suit: Suit.Diamonds
}
This works, because we use the enum when setting the value. If we try to use a random string, it does not work:
const card2: Card = {
value: 5,
suit: "x" // compiler error
}
// error details:
// Type '"x"' is not assignable to type 'Suit'.
// The expected type comes from property 'suit' which is declared here on type 'Card'
It also doesn’t work if we use the same string values as the enum.
const card3: Card = {
value: 5,
suit: "Diamonds" // compiler error
}
// error details:
// Type '"Diamonds"' is not assignable to type 'Suit'.
// The expected type comes from property 'suit' which is declared here on type 'Card'
If value type is an enum, and you manually define a card at compile time, you must use the enum to set the value.
Sometimes numeric enums are desirable. A single integer takes up less space than a string. Sometimes the numeric values are meaningful – it could represent known id values, sort order, or ranking. One possible example is access roles. If all roles are ranked and one always supercedes the other, you could use a numeric enum where the values represent the ranking order:
enum Role {
SuperAdmin = 0,
CompanyAdmin = 1,
Manager = 2,
Employee = 3,
Guest = 4,
}
Then let’s say a user can have multiple roles, like ["Employee", "Manager"]
and we wanted to get that user’s highest privileged role.
First, if the data looks like that instead of numeric values, we can define a typed union with all possible value of a role string, with this handy keyof typeof
syntax:
/**
* This is equivalent to:
* type RoleStrings = 'SuperAdmin' | 'TenantAdmin' | 'Provider' | 'Guest' | 'NoAccess';
*/
type RoleStrings = keyof typeof Role;
Then we can define a function that compares the values of each role and find the one with the lowest value:
function getHighestRole(roles: RoleStrings[]) {
return roles.reduce((acc, role) => {
// if role's number is lower than previous accumulator role's number, it now wins, return it
if (Role[acc] > Role[role]) {
return role
// otherwise return existing accumulator role
} else {
return acc
}
}, "Guest") // default to lowest privileged "Guest" role string
}
Now we can define some data and call the function
const myRoles: RoleStrings[] = ["Manager", "Employee"]
getHighestRole(myRoles) // "Manager"
So, enums provide a way to concretely define and document the set of possible values that a specific variable or object property can be, help programmers avoid mistakes while writing code, and enable compilers to catch errors.
Alright, that’s probably enough about enums for now. By this point it’s either totally clear or you’re totally confused. 🙂