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 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.
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:
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 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
}
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 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:
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.
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"
}
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
}
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!
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"
}
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
*/
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
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
}
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
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
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
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 = {}));
This IIFE propels the following object to runtime:
{
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:
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:
/*
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:
{
"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:
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:
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:
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
:
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.
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;
}
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;
}
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;
}
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";
*/
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";
}
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.