TypeScript of a healthy person, or why Enum is better

Probably most front-end developers at some point faced the task of introducing TypeScript to a project. Usually this task is not performed immediately, but gradually. First, all files are simply renamed from .js to .ts with the type “any” everywhere, just to start the project, and only then gradually the developers begin to engage in systematic translation.

If developers by that time do not have serious experience with TypeScript and study it along the way, then very often the translation ends at the stage of creating a data model, that is, introducing types and interfaces for all basic entities, API typing.

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

So, when creating an interface for an 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 can this create? 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 well-defined and finite number of string values. For example, in our company there is only a director and a salesperson. In case we try to create an object with the position “accountant”, this type of error will not be thrown:

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

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

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

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

Then smart TypeScript will swear when we try to create an accountant:

And the problem seems to be solved, but no.

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 enums that allow the developer to define a set of named constants.

TypeScript provides both numeric and string enums. In this article, we will focus on string Enums.

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

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

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

Having thus created an enumeration of possible positions, we kind of undertook to indicate the position of an employee only through enumeration. That is, now this entry will give an error.

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

Now we indicate the position everywhere like this:

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

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

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

Let’s look at two cases where using Enum gives us interesting additional benefits, in addition to good structuring of the code.

1. Working with Enum as with interfaces.

Suppose we need to divide the employees of the organization by position. For example, let there be a Director employee interface and a Seller employee interface.

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 enum. Let’s write a function that will accept an employee of any of these two types as input and, depending on the value of the position field, return this 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 for our users, the position can take only one of the possible values ​​of Enum Position. And now, with the help of employeeTypeChecker, we can get exactly the type of user we are 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 use extends, we can use conditional types. If the position field were a string, this wouldn’t be possible.

2. Convert enum to array

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

It is very convenient, for example, when we need to give the user the opportunity to select 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 inappropriate and it is more correct to simply write the string type. However, I believe that such cases are much less than cases where the use of enum makes the developer’s life easier.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *