A Detailed Guide on TypeScript Enum

necatiozmen

Necati Özmen

Posted on May 21, 2023

A Detailed Guide on TypeScript Enum

Author: Abdullah Numan

Introduction

Enums 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 enums 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.

TypeScript Enums Examples

In order to illustrate enums 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.

accountTypes 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 enums to define types for these attributes. Using enums 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:

enum AccountType {
    PERSONAL = "Personal",
    STARTUP = "Startup",
    ENTERPRISE = "Enterprise",
    CUSTOM = "Custom"
}
Enter fullscreen mode Exit fullscreen mode

We are using string literals to initialize all enum members in AccountType. This is an example of string enums. Further elaboration ahead in an upcoming section.

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

enum BillingSchedule {
    FREE = 0,
    MONTHLY,
    QUARTERLY,
    YEARLY
}
Enter fullscreen mode Exit fullscreen mode

Here, we are using a numeric literal to initialize the first member of BillingSchedule. This is an example of numeric enums. More on this in a later section.

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

enums Produce Runtime JavaScript Objects

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

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

console.log(accountType); // "Personal"
console.log(billingSchedule); // 0
Enter fullscreen mode Exit fullscreen mode

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.

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:

enum AccountType {
    PERSONAL = "Personal",
    STARTUP = "Startup",
    ENTERPRISE = "Enterprise",
    CUSTOM = "Custom"
}
Enter fullscreen mode Exit fullscreen mode

Numerical Enums in TypeScript

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

enum BillingSchedule {
    // highlight-next-line
    FREE = 0,
    MONTHLY,
    QUARTERLY,
    YEARLY
}
Enter fullscreen mode Exit fullscreen mode

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 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!


refine blog logo


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:

enum AccountType {
    PERSONAL = "Personal",
    STARTUP = "Startup",
    ENTERPRISE = "Enterprise",
    CUSTOM = "Custom"
}
Enter fullscreen mode Exit fullscreen mode

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:

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

/*
Enum member must have initializer.(1061)
(enum member) AccountType.CUSTOM
*/
Enter fullscreen mode Exit fullscreen mode

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:

// Heterogenous enum

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

const accountTypeCustom = AccountType.CUSTOM;
console.log(accountTypeCustom); // 0
Enter fullscreen mode Exit fullscreen mode

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:

// First member initialized, subsequent members auto-increment

enum BillingSchedule {
    // highlight-next-line
    FREE = 0,
    MONTHLY,
    QUARTERLY,
    YEARLY
}
Enter fullscreen mode Exit fullscreen mode

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

console.log(BillingSchedule.MONTHLY); // 1
console.log(BillingSchedule.QUARTERLY); // 2
console.log(BillingSchedule.YEARLY); // 3
Enter fullscreen mode Exit fullscreen mode

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:

// No initialization

enum BillingSchedule {
    // higlight-next-line

    FREE,
    MONTHLY,
    QUARTERLY,
    YEARLY
}


console.log(BillingSchedule.FREE); // 0
Enter fullscreen mode Exit fullscreen mode

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:

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

console.log(BillingSchedule.MONTHLY); // 1
console.log(BillingSchedule.QUARTERLY); // 5
console.log(BillingSchedule.YEARLY); // 6
Enter fullscreen mode Exit fullscreen mode

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:

/*
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 = {}));
Enter fullscreen mode Exit fullscreen mode

This IIFE propels the following object to runtime:

{
    PERSONAL: "Personal",
    STARTUPP: "Startup",
    ENTERPRISEP: "Enterprise",
    CUSTOMP: "Custom"
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

/*
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 = {}));
Enter fullscreen mode Exit fullscreen mode

And it introduces this object to the runtime environment:

{
    "0": "FREE",
    "1": "MONTHLY",
    "2": "QUARTERLY",
    "3": "YEARLY",
    "FREE": 0,
    "MONTHLY": 1,
    "QUARTERLY": 2,
    "YEARLY": 3
}
Enter fullscreen mode Exit fullscreen mode

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

console.log(BillingSchedule.FREE); // 0
console.log(BillingSchedule[0]); // "FREE"
console.log(BillingSchedule.YEARLY); // 3
console.log(BillingSchedule[3]); // "YEARLY"
Enter fullscreen mode Exit fullscreen mode

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:

enum AccountType {
    PERSONAL = "Personal",
    STARTUP = "Startup",
    ENTERPRISE = "Enterprise",
    CUSTOM = "Custom"
}
Enter fullscreen mode Exit fullscreen mode

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

enum BillingSchedule {
    FREE = 0,
    MONTHLY,
    QUARTERLY,
    YEARLY
}
Enter fullscreen mode Exit fullscreen mode

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

enum BillingSchedule {
    FREE,
    MONTHLY,
    QUARTERLY,
    YEARLY
}
Enter fullscreen mode Exit fullscreen mode

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.

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:

enum ABasicExample {
    A_BASIC_EXAMPLE = "A Basic Example".length;
}
Enter fullscreen mode Exit fullscreen mode

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:

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

interface IStartupAccount {
    // highlight-next-line
    tier: AccountType.STARTUP;
    postsQuota: number;
    verified: boolean;
}
Enter fullscreen mode Exit fullscreen mode

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:

interface IFreeBilling {
    tier: BillingSchedule.FREE;
    startDate: string | boolean;
    expiryDate: string | boolean;
}
Enter fullscreen mode Exit fullscreen mode

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:

/*
    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";
*/
Enter fullscreen mode Exit fullscreen mode

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:

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";
}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
necatiozmen
Necati Özmen

Posted on May 21, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related