Published on

TypeScript: Using Enums, a complete guide

Enums are an essential feature in TypeScript, allowing you to define a set of named constants that represent a finite set of values. Enums can be used in various scenarios, such as defining a set of options for a dropdown or radio button selection or representing different types of data.

In this guide, we will explore how to use Enums in TypeScript with a practical example. We will define an interface for a member with a subscription and use Enums to represent different types of subscriptions.

Table of Contents

Defining an Interface with Enums

Let's start by defining an interface for a member with a subscription. We will use an interface called Member to represent a member's details, including their full name, subscription ID, and subscription type.

Example:

ts-enum.tsx
interface Member {
    fullName: string;
    subscriptionID?: number;
    subscriptionType :SubscriptionType
}

In this interface, we have three properties:

fullName: a required string property that represents the member's full name. subscriptionID: an optional number property that represents the member's subscription ID. subscriptionType: a required property that represents the member's subscription type. Now, let's define an Enum for subscription types.

ts-enum.tsx
enum SubscriptionType {
    Classic,
    Premium,
    Gold,
}

In this scenario, the Enum called SubscriptionType has three members: Classic, Premium, and Gold. These members are assigned numeric values by default, starting from 0 for the first member and incrementing by 1 for each subsequent member. However, you can assign custom values to members as well.

Using Enums as Attributes

Now that we have defined our interface and Enums, let's see how to use them. We can create a new Member object with the following code:

ts-enum.tsx
const member: Member = {
    fullName: 'John Smith',
    subscriptionType: SubscriptionType.Premium,
}

Above, the new Member object with the full name 'John Smith' and a subscriptionType of SubscriptionType.Premium is created. Since we did not specify a subscriptionID, it will be undefined.

We can also use Enums to switch between different subscription types and perform different actions based on the selected subscription type.

Example:

ts-function.tsx
const getSubscriptionCost = (subscriptionType: SubscriptionType): number => {
    switch (subscriptionType) {
        case SubscriptionType.Classic:
        return 10
        case SubscriptionType.Premium:
        return 20
        case SubscriptionType.Gold:
        return 30
        default:
        throw new Error('Invalid subscription type.')
    }
}

const subscriptionType = SubscriptionType.Gold
const subscriptionCost = getSubscriptionCost(subscriptionType)
console.log(`Subscription cost: ${subscriptionCost}`)

The arrow function called getSubscriptionCost takes a subscriptionType parameter of type SubscriptionType and returns the corresponding subscription cost. We use a switch statement to check the subscriptionType parameter and return the appropriate cost.

A variable called subscriptionType holds the value of SubscriptionType.Gold. This value is been passed to the getSubscriptionCost() arrow function. The result 30 is filtered by the switch statement.

Converting Enum Keys or Values into an Array

There are several scenarios where you might want to convert an Enum's keys or values into an array:

  1. Displaying Enum values in a dropdown or a list: You might want to display all the available options of an Enum in a dropdown or a list, which requires converting Enum keys or values into an array.

  2. Looping over Enum values: If you need to perform some operations or validations on each Enum value, you might want to loop over the Enum values by converting them into an array.

  3. Converting Enum keys or values into a string: If you need to serialize an Enum or pass its values as a string to some API, you might want to convert Enum keys or values into a comma-separated string.

Here is an example of how to convert an Enum's values :

ts-convert-enum.tsx

enum SubscriptionType {
    Classic,
    Premium,
    Gold,
}

//Values
const subTypesValuesArray = Object.values(SubscriptionType)
console.log(subTypesValuesArray) // Returns: ["Classic", "Premium", "Gold", 0, 1, 2]
//Keys
const subTypesKeysArray = Object.keys(SubscriptionType)
console.log(subTypesKeysArray) // Returns: ["0", "1", "2", "Classic", "Premium", "Gold"]

//Get Keys as Array: Method A
const subscriptionTypesArray = Object.keys(SubscriptionType)
                                     .filter((k) => typeof SubscriptionType[k as any] === 'number')
console.log(subscriptionTypesArray) // Returns: ["Classic", "Premium", "Gold"]

//Get Keys as Array: Method B
const subscriptionTypesArray2 = Object.keys(SubscriptionType)
                                      .filter((value) => isNaN(Number(value)) === false)
                                      .map((key) => SubscriptionType[key])
console.log(subscriptionTypesArray2) // Returns: ["Classic", "Premium", "Gold"]


Evaluating Enum values of instantiated objects

By reusing above Method B we can create a getValidSubsTypes function that can return all enum keys in an Array. From there if we will need to validate an existing value agains that array we can use memberHasValidSubscription to confirm if it matches with any Enum value. Here there's an example how to validate

ts-validate-enum.tsx
  interface Member {
    fullName: string
    subscriptionID?: number
    subscriptionType: SubscriptionType // ❌ just to allow a falsy entry for the example purpose
    //subscriptionType: SubscriptionType.Classic | SubscriptionType.Premium | SubscriptionType.Gold //✅ prefered solution
  }

  enum SubscriptionType {
    Classic,
    Premium,
    Gold,
  }

  const member: Member = {
    fullName: 'Johnny Smith',
    subscriptionType: SubscriptionType.Gold,
  }

  const memberFromUI: Member = {
    fullName: 'Mike Smith',
    subscriptionType: 5,
  }

  const isValidSubscriptionType = (subscriptionType: SubscriptionType) => {
    return Object.values(SubscriptionType).includes(subscriptionType)
  }

  const memberHasValidSubscription = (member: Member): boolean => {
    if (!isValidSubscriptionType(member.subscriptionType)) {
     throw new Error(`${member.fullName} has Invalid subscription type`)
    }
    return true
  }

  try {
    console.log(memberHasValidSubscription(member)) //Returns true
    console.log(memberHasValidSubscription(memberFromUI)) //Throws Error 🚫
  } catch (e) {
    console.error('%s', e.message) //Mike Smith has Invalid subscription type
}

Enums are a powerful feature in TypeScript that allow you to define a set of named constants. In this guide, we have explored how to define an interface with an Enum property and use Enums to represent different types of subscriptions. We have also seen how to switch between different subscription types and perform different actions based on the selected subscription type.

With these examples, you should now be able to start using Enums in your TypeScript projects and leverage their power to make your code more readable and maintainable.

EXTRA: Alternative Best Practices

When it comes to defining a set of related values in TypeScript, many developers instinctively reach for the enum keyword. Enums are a convenient way to give names to values, but they have several limitations that can make them less than ideal for certain use cases. One alternative that is worth considering is the use of union types.

Union types allow you to define a type that can represent one of several different possible values, making them more flexible and easier to work with than enums in many situations.

Consider the example of defining the Member interface that includes the subscriptionType field. Using an enum to define the possible subscription types might seem like a natural choice, but this approach has some drawbacks

For one thing, enums are limited to a fixed set of values that are defined at compile time. If you need to add a new subscription type later, you will have to modify and recompile your code. Additionally, enums can be verbose and repetitive to work with, as you have to use the enum name to refer to each value.

A better approach in this case would be to define subscriptionType as a union type, using string literals to represent the possible values.

Here's an example of how this could look:

ts-union-alt.tsx
interface Member {
    fullName: string;
    subscriptionID?: number;
    subscriptionType: 'Classic' | 'Premium' | 'Gold'
}

With this approach, you will get all the benefits of enums (named values, type safety, etc.) without their limitations. You can easily add new subscriptionType simply by adding a new string literal to the union and you don't have to worry about runtime issues like Enum value collisions.

Overall, while enums have their uses, it's worth considering the benefits of using union types in situations where you need more flexibility and less boilerplate.