A Detailed Guide on TypeScript Enum
Tags: typescript, javascript, programming, webdev
Published: 2023-05-21 12:52:31

**Author: <a  target="_blank" href="https://refine.dev/blog/author/abdullah_numan/">Abdullah Numan</a>**



## Introduction

**Enum**s are constants based data structures that store a set of named constants grouped around a central theme or intent. In TypeScript,  Enums are a feature that injects runtime JavaScript objects to an application in addition to providing usual type-level extensions.

This post explores **enums** in TypeScript with examples from a tiers based Subscription model where subscription entities differ according to account types and billing schedules.

While examining the examples, we discuss underlying **enums** concepts including **`enum` member** types such as **string** or **numeric** and **constant** or **computed**; **homogeneity** and **heterogeneity** of `enum`s as well as **member initialization** with or without setting a value. We also explore how an enum translates to an IIFE during compilation, carries out **directional mapping** and injects its JavaScript object to the runtime environment. We examine and leverage the indivdual types generated by enum members to define our own subtypes and see how the main enum type generates a union of member keys. Lastly, we bring all these enum concepts together to implement a simple `PersonalSubscription` class.

In the sections ahead, we relate to examples for the tiers based Subscription model and analyze them to discuss underlying concepts and behaviors.



## Prerequisites

In order to properly follow this post and test out the examples, you need to have a JavaScript engine. It could be **Node.js** in your local machine with TypeScript supported or you could use the [TypeScript Playground](https://www.typescriptlang.org/play#code).


## TypeScript Enums Examples

In order to illustrate `enum`s concepts in TypeScript, we are using a tiers based Subscription model. Let's say, we have a subscription entity stored in a `subscriptions` table. And it has `accountType` and `billingSchedule` attributes.

`accountType`s can be one of `Personal`, `Startup`, `Enterprise` or `Custom`. `billingSchedule` can be categorized as one of `Free`, `Monthly`, `Quarterly` or `Yearly`. These possible options indicate an intent to group a subscription based on account type and billing schedule. We can use TypeScript `enum`s to define types for these attributes. Using `enum`s not only allows us to declare types for `accountType` and `billingSchedule`, but also creates representative runtime objects which would otherwise need to be produced from a database table for reference.

So, to start the proceedings let's define a couple of enums. We can declare the `enum` for `accountType` attribute like this:

```tsx
enum AccountType {
    PERSONAL = "Personal",
    STARTUP = "Startup",
    ENTERPRISE = "Enterprise",
    CUSTOM = "Custom"
}
```

We are using `string` literals to initialize all `enum` members in `AccountType`. This is an example of **string enum**s. Further elaboration ahead in [an upcoming section](#string-enums-in-typescript).

One way of defining an `enum` for `billingSchedule` looks like this:

```tsx
enum BillingSchedule {
    FREE = 0,
    MONTHLY,
    QUARTERLY,
    YEARLY
}
```

Here, we are using a numeric literal to initialize the first member of `BillingSchedule`. This is an example of **numeric enum**s. More on this in [a later section](#numerical-enums-in-typescript).

Let's quickly test out the runtime role of enums as we start discussing underlying `enum`s concepts.


### `enum`s Produce Runtime JavaScript Objects

We mentioned before that `enum`s inject JS objects to runtime environment. This can be observed when we run the following snippet:

```tsx
const accountType = AccountType.PERSONAL;
const billingSchedule = BillingSchedule.FREE;

console.log(accountType); // "Personal"
console.log(billingSchedule); // 0
```

With `AccountType.PERSONAL` and `BillingSchedule.FREE`, we are calling actual objects at runtime and getting appropriate responses. These indicate that TypeScript `enum` definitions are not just simple type definitions, but also introduce JS objects to our application. We revisit this in [a later part of this post](#typescript-enums-at-compile-time-and-runtime).


## `enum` Types in TypeScript

Enum members are typically used to store constants. Members can have string constants, numerical constants or mix of both. Homogeneity of member values determines whether the enum is a **string enum** or a **numerical enum**.


### String Enums in TypeScript

When all members of an enum have string values, it is a string enum. As in `AccountType`:

```tsx
enum AccountType {
    PERSONAL = "Personal",
    STARTUP = "Startup",
    ENTERPRISE = "Enterprise",
    CUSTOM = "Custom"
}
```


### Numerical Enums in TypeScript

Similarly, when all members have numerical values, the enum itself becomes numerical:

```tsx
enum BillingSchedule {
    // highlight-next-line
    FREE = 0,
    MONTHLY,
    QUARTERLY,
    YEARLY
}
```

Here, `BillingSchedule` has the first member initialized to a number, and the subsequent ones are uninitialized but TypeScript's enum defaults auto-increment them by `1`. So, all members here are numerical and ` BillingSchedule` is a numerical enum. We discuss this more in the next section on enum member initialization.

---


## Open-source enterprise application platform for serious web developers

[refine.new](https://refine.new/) enables you to create React-based, headless UI enterprise applications within your browser that you can preview, tweak and download instantly.

🚀 By visually combining options for your preferred ✨ React platform, ✨ UI framework, ✨ backend connector, and ✨ auth provider; you can create tailor-made architectures for your project in seconds. It feels like having access to thousands of project templates at your fingertips, allowing you to choose the one that best suits your needs!

 

<div>
<a href="https://refine.new/">
   <img  src="https://refine.ams3.cdn.digitaloceanspaces.com/website/static/img/quick-start.gif" alt="refine blog logo" /> 
</a>
</div>

---



## Enum Member Initialization in TypeScript

String enum members must be initialized explicitly with string values. In numerical enums, they may remain uninitialized and the value is then assigned implicitly by TypeScript.


### Member Initialization in TypeScript String Enums

For string enums, as we can see in our `AccountType` example, we are explicitly initializing all members:

```tsx
enum AccountType {
    PERSONAL = "Personal",
    STARTUP = "Startup",
    ENTERPRISE = "Enterprise",
    CUSTOM = "Custom"
}
```

Here, we are using string literals to meaningfully document and describe our intent of grouping account types in to several options, which is useful for our application features and developer experience. Explicit string initialization also helps with serialization of the JS object created at runtime.

An unitialized member coming after a string member is invalid:

```tsx
enum AccountType {
    PERSONAL = "Personal",
    STARTUP = "Startup",
    ENTERPRISE = "Enterprise",
    CUSTOM
}

/*
Enum member must have initializer.(1061)
(enum member) AccountType.CUSTOM
*/
```

If `CUSTOM` is uninitialized as the first member, it is assigned `0` by default, but we then have a **heterogenous** enum with a numeric member mixed with string members:

```tsx
// Heterogenous enum

enum AccountType {
    CUSTOM,
    PERSONAL = "Personal",
    STARTUP = "Startup",
    ENTERPRISE = "Enterprise"
}

const accountTypeCustom = AccountType.CUSTOM;
console.log(accountTypeCustom); // 0
```


### Member Initialization in TypeScript Numerical Enums

Members in a numerical enum may or may not be initialized explicitly. Uninitialized members are assigned implicit default values.


**Explicitly Initialized Numerical Members**

In our `BillingSchedule` enum, we explicitly assigned `0` to the first member:

```tsx
// First member initialized, subsequent members auto-increment

enum BillingSchedule {
    // highlight-next-line
    FREE = 0,
    MONTHLY,
    QUARTERLY,
    YEARLY
}
```

The subsequent members get an auto-incremented value increased by `1`:

```tsx
console.log(BillingSchedule.MONTHLY); // 1
console.log(BillingSchedule.QUARTERLY); // 2
console.log(BillingSchedule.YEARLY); // 3
```

As we can see, initializing a member with a number represents an offset value based on which auto-incremented values of subsequent members are determined. Assigning the first member with `0` represents a zero offset. We could have been better off without initialization of a member at all, like this:

```tsx
// No initialization

enum BillingSchedule {
    // higlight-next-line

    FREE,
    MONTHLY,
    QUARTERLY,
    YEARLY
}


console.log(BillingSchedule.FREE); // 0
```

This is because the default offset for first member is `0`. This definition offers more convenience.

We can assign an offset anywhere and it would reflect in subsequent implicitly incremented member values:

```tsx
enum BillingSchedule {
    FREE,
    MONTHLY,
    QUARTERLY = 5,
    YEARLY
}

console.log(BillingSchedule.MONTHLY); // 1
console.log(BillingSchedule.QUARTERLY); // 5
console.log(BillingSchedule.YEARLY); // 6
```


## TypeScript Enums at Compile Time and Runtime

At compile time, TypeScript translates an enum to a corresponding IIFE which then introduces into runtime a JavaScript object representation of the enum.

String members and numeric members behave differently at compilation. A string member gets mapped **unidirectionally** to its corresponding JavaScript object property. In contrast, a numeric member gets mapped **bi-directionally** to its runtime object property. So, as we see in the sections below, string enums are limited to unidirectional navigation, but numeric members offer us the convenience of bidirectional access to constants.


###  TypeScript String Enums Have Unidirectional Mapping

The `AccountType` enum we declared in the beginning compiles to the following JS code:


```tsx
/*
enum AccountType {
    PERSONAL = "Personal",
    STARTUP = "Startup",
    ENTERPRISE = "Enterprise",
    CUSTOM = "Custom"
}
*/

"use strict";
var AccountType;
(function (AccountType) {
    AccountType.PERSONAL = "Personal";
    AccountType["STARTUP"] = "Startup";
    AccountType["ENTERPRISE"] = "Enterprise";
    AccountType["CUSTOM"] = "Custom";
})(AccountType || (AccountType = {}));
```

This IIFE propels the following object to runtime:

```tsx
{
    PERSONAL: "Personal",
    STARTUPP: "Startup",
    ENTERPRISEP: "Enterprise",
    CUSTOMP: "Custom"
}
```

Unidrectional mapping of a string member sets only the constant names as keys and therefore limits access to the enum only via constant names only, not by the value:

```tsx
console.log(AccountType.PERSONAL); // "Personal"
console.log(AccountType.Personal); // Property 'Personal' does not exist on type 'typeof AccountType'. Did you mean 'PERSONAL'?(2551)
```

Accessing the enum via member values is possible in numeric enums, as we'll see next.


### TypeScript Numerical Enums Have Bidirectional Mapping

In contrast to unidirectional mapping of string enums, numerical enums compile to bidirectional JS objects. Our `BillingSchedule` object translates to the following IIFE:

```tsx
/*
enum BillingSchedule {
    FREE,
    MONTHLY,
    QUARTERLY = 5,
    YEARLY
}
*/

"use strict";
var BillingSchedule;
(function (BillingSchedule) {
    BillingSchedule[BillingSchedule["FREE"] = 0] = "FREE";
    BillingSchedule[BillingSchedule["MONTHLY"] = 0] = "MONTHLY";
    BillingSchedule[BillingSchedule["QUARTERLY"] = 0] = "QUARTERLY";
    BillingSchedule[BillingSchedule["YEARLY"] = 0] = "YEARLY";
})(BillingSchedule || (BillingSchedule = {}));
```

And it introduces this object to the runtime environment:

```tsx
{
    "0": "FREE",
    "1": "MONTHLY",
    "2": "QUARTERLY",
    "3": "YEARLY",
    "FREE": 0,
    "MONTHLY": 1,
    "QUARTERLY": 2,
    "YEARLY": 3
}
```

So, now we are able to navigate both ways for numeric members:

```tsx
console.log(BillingSchedule.FREE); // 0
console.log(BillingSchedule[0]); // "FREE"
console.log(BillingSchedule.YEARLY); // 3
console.log(BillingSchedule[3]); // "YEARLY"
```

## Enum Member Values in TypeScript: Constant vs Computed

Enum member values can be **constant** or **computed**.

### Constant Values of Enum Members

In both our examples, the value of enums are constant. However, there are subtle differences among constant values too. For example, in the `AccountType` enum, all values are string literals which are considered as **literal enum expressions**:

```tsx
enum AccountType {
    PERSONAL = "Personal",
    STARTUP = "Startup",
    ENTERPRISE = "Enterprise",
    CUSTOM = "Custom"
}
```

Similarly, in the following `BillingSchedule` enum, the numeric literal `0` is also a literal enum expression:

```tsx
enum BillingSchedule {
    FREE = 0,
    MONTHLY,
    QUARTERLY,
    YEARLY
}
```

Uninitialized members which implicitly get assigned numeric literals are also considered constants. As all the members have in this version of `BillingSchedule`:

```tsx
enum BillingSchedule {
    FREE,
    MONTHLY,
    QUARTERLY,
    YEARLY
}
```

There are other nuanced forms of literal enum expressions, such as a value referenced from another enum member. For the rest of the possible cases, please look up the [TypeScript **Enums** docs](https://www.typescriptlang.org/docs/handbook/enums.html).


### Computed Values of Enum Members

A computed value is assumed when a member's value is computed from a JavaScript expression. We have no such use case in our examples, but a basic instance would look like this:

```tsx
enum ABasicExample {
    A_BASIC_EXAMPLE = "A Basic Example".length;
}
```


## Types from TypeScript Enums

So far, we have explored only the object aspects of TypeScript enums. Let's now consider types act out in enums.

When all members of an enum are literal enum expressions, types for each member are generated from their member names. And the enum itself effectively becomes a union of all the subtypes.


### Individual Types

Individual types are generated from each member when all members of the enum are either string literals or numeric literals. This becomes clear when such standalone types are used to define new subtypes. For example, from our `AccountType` enum, we can produce a few account subtypes which uses the member types:

```tsx
type TPersonalAccount = {
    // highlight-next-line
    tier: AccountType.PERSONAL;
    postsQuota: number;
    verified: boolean;
}

interface IStartupAccount {
    // highlight-next-line
    tier: AccountType.STARTUP;
    postsQuota: number;
    verified: boolean;
}
```

In the above type definitions, we are using `AccountType.PERSONAL` and `AccountType.STARTUP` enum member types to define new subtypes of accounts.

In a similar vein, let's look at a subtype derived from a `BillingSchedule` member:

```tsx
interface IFreeBilling {
    tier: BillingSchedule.FREE;
    startDate: string | boolean;
    expiryDate: string | boolean;
}
```


### Union of Member Keys

The type generated by the enum itself is effectively a union of all enum member types. It can be accessed with the `keyof typeof` functions chained like this:

```tsx
/*
    enum AccountType {
        PERSONAL = "Personal",
        STARTUP = "Startup",
        ENTERPRISE = "Enterprise",
        CUSTOM = "Custom"
    }
*/

type TAccountType = keyof typeof AccountType;

/*
The generated type is equivalent to:

    type TAccountType = "PERSONAL" | "STARTUP" | ENTERPRISE | "ENTERPRISE" | "CUSTOM";
*/
```

The code above first accesses the enum object with `typeof` and then grabs the member names (keys) with `keyof`.

With these essential concepts covered, let's now see how to use enums and its generated types inside TypeScript classes.


## Using TypeScript Enums in Classes

We can now refactor some of the enums and type definitions and implement a rudimentary `PersonalSubscription` class which uses them:

```tsx
enum AccountType {
    PERSONAL = "Personal",
    STARTUP = "Startup",
    ENTERPRISE = "Enterprise",
    CUSTOM = "Custom"
}

enum BillingSchedule {
    FREE,
    MONTHLY,
    QUARTERLY,
    YEARLY
}

type TAccount<AccountType> = {
    tier: AccountType;
    postsQuota: number;
    verified: boolean;
}

interface IBilling<BillingSchedule> {
    tier: BillingSchedule;
    startDate: string | boolean;
    expiryDate: string | boolean;
}

class PersonalAccount implements TAccount<AccountType.PERSONAL> {
    tier: AccountType.PERSONAL = AccountType.PERSONAL;
    postsQuota = 2;
    verified = false;
}

class FreeBilling implements IBilling<BillingSchedule.FREE> {
    tier: BillingSchedule.FREE = BillingSchedule.FREE;
    startDate = false;
    expiryDate = false;
}

interface IPersonalSubscription<TAccount, IBilling> {
    accountType: TAccount;
    billingSchedule: IBilling;
    creditCard: string;
}

class PersonalSubscription implements IPersonalSubscription<TAccount<AccountType.PERSONAL>, IBilling<BillingSchedule.FREE>> {
    accountType = new PersonalAccount();
    billingSchedule = new FreeBilling();
    creditCard: string = "XXXXXXXXXXXXXXXX";
}
```

In the above code, for `BillingSchedule` we have used a numeric enum with all uninitialized members. The first member is therefore assigned `0` and subsequent ones get auto-incremented by `1`. We have used generics to pass in `AccountType` and `BillingSchedule` types to `TAccount` and `IBilling` respectively so their use becomes more flexible in the `PersonalAccount` and `FreeBilling` classes as well as in the `IPersonalSubscription` type, where we are using enum members both as constant values as well as type definitions.


## Summary

In this post, we explored TypeScript enum concepts by storing groups of constants in a couple of enums defined to implement a simplistic tier based Subscription model. We stored constants in enums for `AccountType` and `BillingSchedule`. On our way, we found that it is mandatory to initialize every member in a string enum, and which is not necessary in a numeric enum. We saw how an uninitialized first member is automatically assigned an offset of `0` and subsequent members get auto-incremented by `1`. We learned how to assign offset values at any point in a numeric enum.

We also demonstrated how string enums implement unidirectional mapping and numeric enums implement a more convenient unidirectional mapping at compiltion and got an idea of typical objects introduced by them to runtime. We discussed the common usage of literal enum expressions in declaring string and numeric enums with constant values, and how they differ from computed member values.

Towards the end, we explored the types generated by the enums and leveraged them to derive our own subtypes. Finally we implemented a basic `PersonalSubscription` class that demonstrates the convenience offered by objects and types generated by TypeScript enums.