You might be wondering what enums are in TypeScript, well in this post we will cover just what they are, along with how and why you might want to use them in your application.
What are enums in TypeScript in plain english
An enum in TypeScript is a collection of constants that you can reference to get their value, in JavaScript you could imagine them as an object containing a single layer of keys and values. In TypeScript string and numerical enums are currently supported.
TypeScript is an extension of JavaScript which usually is used for the type-level extensions (to add types to our JavaScript), however enums are not actually a type-level extension meaning they can be used as a value as well as a type.
enum ExampleEnum {
HELLO = "Hello",
WORLD = "World",
}
ExampleEnum.HELLO // "Hello"
ExampleEnum.WORLD // "World"
So what does a collection of constants really mean here?
Well in JavaScript and TypeScript any time you see or use the const
declaration, you are creating a constant which means from that point onward it will be read-only so that it’s value cannot be changed.
Whereas a variable like a let
or a var
will enable you to be able to change the value later on.
const i = 0 // 0
i = 1 // Uncaught TypeError: Assignment to constant variable.
As you can see in this error, we cannot re-assign a value to a const that has already been declared.
And if we do this with a let
instead:
let i = 0 // 0
i = 1 // 1
With a let (or a var) we no longer get an error because we can read and write to these variables even after they have been declared.
So when I say a collection of constants it means we will have a collection of read-only variables that can be accessed like you would an object.
The reason why enums in TypeScript are known as a “collection” or a “set” is because they aren’t an array, and they aren’t a regular object either.
Using the above example, here is roughly how it would look if you were to implement this with an object instead:
const ExampleEnumObject = {
HELLO: "Hello",
WORLD: "World",
}
ExampleEnumObject.HELLO // "Hello"
ExampleEnumObject.WORLD // "World"
So if we can create it with an object, why do we need to use an enum?
Whilst they do look similar, an object (aside from assigning it to the constant variable) is not read-only meaning you can update and change the object and its values.
const ExampleEnumObject = {
HELLO: "Hello",
WORLD: "World",
}
ExampleEnumObject.HELLO // "Hello"
ExampleEnumObject.HELLO = "Oops"
ExampleEnumObject.HELLO // "Oops"
Whilst there might be ways around this in JavaScript to make an object like this read-only, with Object.freeze for example, we won’t be going into that here because we are going to be focusing on enums in this post.
How to use enums in TypeScript
So now we understand what enums in TypeScript are, let’s look at how we can actually use them in our code and what they can do.
Firstly, let’s take a look at how we might define an enum.
Just like with types, in most cases we would create them using Upper Camel Case, but this is going to be dependent on the naming conventions of your codebase as well.
Let’s start by creating an empty enum that we export:
export enum CoffeeBeans {}
Okay so now we have an empty enum, it is time to add our constants to it.
When you name your constants, once again, it is dependent on your codebase and the naming conventions used within it but in most cases I have seen they would usually either mimic whatever the value will be if it is possible to do so or just use Upper Camel Case.
So if you have a string "HELLO_WORLD"
, you would also name your constant in the same way, HELLO_WORLD
or HelloWorld
.
For this example, we will use the Upper Camel Case naming convention again though.
export enum CoffeeBeans {
Columbian = "Columbian",
Ethiopian = "Ethiopian",
}
Now that we have a completed enum, the next thing we need to do is to start using it in our code.
Firstly, let’s create a function in TypeScript that accepts this enum as an argument.
function grindCoffee(coffeeBean: CoffeeBeans) {
return `Ground ${coffeeBean} coffee`
}
This is where enums are a little different to other types in TypeScript, and that is because they aren’t just a type.
We can actually use an enum for both a value and a type, which is what we are doing here.
Our grindCoffee
function accepts a value from the enum that we created which can be any of the constants defined within it, but to provide that value we also need to pass in one of the enum constants as a value.
Here is how we can do that:
export enum CoffeeBeans {
Columbian = "Columbian",
Ethiopian = "Ethiopian",
}
function grindCoffee(coffeeBean: CoffeeBeans) {
return `Ground ${coffeeBean} coffee`
}
grindCoffee(CoffeeBeans.Columbian) // "Ground Columbian coffee"
At this point you might be asking why we added the export statement to the enum, and that is because in most cases we are going to want to be able to access and use that enum from many parts of our application.
The reason behind this is so that we don’t even have to type out these strings again which can help prevent errors and bugs whilst keeping our code as strongly typed as possible.
Different types of enums in TypeScript
In TypeScript there are three kinds of enums that are supported.
Firstly, as we can see in the above example, there are string enums, which contain only strings.
Then we have numeric enums which contain only numbers.
And lastly, we have heterogeneous enums which means we can have a combination of strings and numbers in a single enum.
Generally speaking though you should aim to avoid using heterogeneous enums because they can lead to confusion and in most cases are not needed.
Out of all three different types of enum, the one that you should look at using for the majority of cases are string enums because they are easy to read, debug and have the most contextual value whereas numbers are not always as obvious.
An example of what this means is that if you wanted an enum with two values/states to match binary, so on
or off
, you might be tempted to use 1
or a 0
for the value.
Whilst this might make sense now it does not provide much context when debugging or logging, so it would be better to use strings set to "ON"
or "OFF"
.
Why should you use enums in TypeScript
Enums are a powerful way to ensure the consistency of a value passed around your application.
One of the best times to use them could be for if statements or switch cases.
By using them in these situations you can avoid “magic strings” which is where you write strings rather than provide a typed value.
Here is an example of a magic string:
let magicString = 'magic';
if (magicString === 'magic') { ... }
switch(magicString) {
case 'magic':
return (...)
}
And now let’s look at how that exact example would look like if we used enums instead:
enum StrongEnum {
Strong = 'Strong'
}
let strongString = StrongEnum.Strong;
if (strongString === StrongEnum.Strong) { ... }
switch(magicString) {
case StrongEnum.Strong:
return (...)
}
As you can see, we are removing room for error by always referring to typed constants rather than magic strings that could be anything.
A good example of a real life application for using enums in this way would be using them for action types in reducers and actions.
This could be in react when using redux, or with useReducer but it would avoid the use of magic strings or having to export hundreds of const
values from a file.
For example:
enum ActionTypes {
SET_ID = 'SET_ID',
SET_NAME = 'SET_NAME'
}
const myReducer = (state, action) => {
switch(action.type) {
case ActionTypes.SET_ID:
return (...)
case ActionTypes.SET_NAME:
return (...)
default:
return state;
}
}
export default function MyComponent() {
const [state, dispatch] = useReducer(myReducer, {})
const onClick = () => dispatch({
type: ActionTypes.SET_ID,
payload: 'some_id',
})
return (...)
}
Summary
And there we have the basics of what an enum is in TypeScript and how to use it.
If you get the opportunity to make use of them, I highly recommend it because they almost always save a lot of pain.