TypeScript of a healthy person, or why Enum is better

Short description

Front-end developers often implement TypeScript gradually in projects, first renaming .js files to .ts and inserting type “any”. However, issues arise when describing properties that can have finite string values, as developers often only specify the type “string” or list possible values through “or” in interfaces. This can lead to errors when creating objects with incorrect values. Using string Enums can help solve this issue by defining a set of named constants that can be easily maintained and used to specify values in interfaces. Enums also provide additional benefits such as working with interfaces and easily obtaining an array of values.

TypeScript of a healthy person, or why Enum is better

Most front-end developers have probably faced the task of implementing TypeScript on a project at some point. Usually, this task is not performed immediately, but gradually. At first, all files are simply renamed from .js to .ts with the type “any” inserted everywhere, just so that the project will start, and only then gradually the developers begin to deal with systematic translation.

If developers at that time do not have serious experience with TypeScript and learn it in practice, very often the translation ends at the stage of creating a data model, that is, introducing types and interfaces of all the main entities, API typing.

Most often, when creating types and interfaces, describing some property that can acquire a certain, finite number of string values, developers specify the type of the field “string” or, in extreme cases, list these values ​​through “or”.

So, when creating an interface for any employee who has a name, age and position in the company, the simplest and fastest option is presented below:

interface Person {
    name: string;
    age: number;
    position: string;
  }

There are no errors. Everything seems to work, but what problems could this cause? If the name is a string that can take any value, then the position in the company is also a string, but it can only take a certain and finite number of string values. For example, our company has only a director and a salesperson. If we try to create an object with the position “accountant”, this type of error will not be given:

const person: Person = {
  name: 'Иван',
  age: 35,
  position: 'Бухгалтер'
}

The easiest and fastest (but incorrect) way to solve this problem is to create a conditional type and list all possible values ​​in the type:

  type Position = 'Директор' | 'Продавец';

  interface Person {
    name: string;
    age: number;
    position: Position;
  }

Then the clever TypeScript fights when we try to create an accountant:

And it seems that the problem is solved, but it is not.

And, as you probably understood from the title of the article, all these problems can be solved using such a wonderful part of TypeScript as Enums.

According to the documentation, Enums are enumerations that allow a developer to define a set of named constants.

TypeScript provides both numeric and string enumerations. This article will focus specifically on string Enums.

In a string enumeration, each member must be constant-initialized with a string literal or another member of the string enumeration. In our case, the string enum we use instead of the Position type would look like this:

  enum Position {
    Director="Директор",
    Seller="Продавец"
  }

  interface Person {
    name: string;
    age: number;
    position: Position;
  }

Having created a list of possible positions in this way, we only undertook to indicate the employee’s position only through the list. That is, now this record will give an error.

Because now the “Director” string is just some string that has nothing to do with the Position List.

Now we indicate the position everywhere like this:

const person: Person = {
    name: 'Иван',
    age: 35,
    position: Position.Director
  }

And if the position “Director” in our company changes to “General Director”, then the change will need to be implemented in only one place – Enum.

enum Position {
  Director="Генеральный директор",
  Seller="Продавец"
}

Let’s consider two cases where the use of Enum gives us interesting additional advantages in addition to good structuring of the code.

1. Work with Enum as with interfaces.

Let’s say we need to divide the organization’s employees from positions. For example, let there be an employee interface Director and an employee interface Seller.

interface Director {
  position: Position.Director;
  name: string;
  salary: number;
};

interface Seller {
  position: Position.Seller;
  name: string;
  salary: number;
  product: string;
}

As before, they have a position field, which is defined via an enum. Let’s write a function that will take as input an employee of either of these two types and, depending on the value of the position field, return that employee with one of the specified types.

function employeeTypeChecker<T extends Position>(
  position: T, employee: Director | Seller 
) {
  if (position === Position.Director) {
    return employee as T extends Position.Director ? Director : never
  } else {
    return employee as T extends Position.Seller ? Seller : never;    
  }
}

Now let’s create two users with an unknown type, but with a well-defined position field.

const user1 = {
  position: Position.Seller as const,
  name: 'Mary',
  salary: 5000,
  product: 'Phone'
} 

const user2 = {
  position: Position.Director as const,
  name: 'John',
  salary: 10000,
} 

Please note that only one of the possible values ​​of Enum Position can take the position of our users. And now, with employeeTypeChecker, we can get exactly what type of user we’re dealing with in each case.

This is made possible by the fact that in the employeeTypeChecker function we work with Enum as an interface. We can apply extends, you can use conditional types. If the position field were a string, this would not be possible.

2. Translation of an enum into an array

Another useful case that Enum gives us is an easy way to get an array of all its possible values. Since Enum is inherently an object, using Object.values ​​(Enum) gives us an array of Enum string values.

Very convenient, for example, when we need to give the user the opportunity to choose a value from all possible ones using the select tag.

Of course, enum is not a panacea, and there are cases when their use is impractical and it is more correct to simply write the string type. However, I believe that these cases are far fewer than the cases where using enums makes a developer’s life easier.

Related posts