📜 ⬆️ ⬇️

Nominative typing in TypeScript or how to protect your interface from foreign identifiers


Recently, studying the reasons for the incorrect work of my home project, I once again noticed a mistake, which is often repeated because of fatigue. The essence of the error is that, having several identifiers in one block of code, when I call a certain function, I pass the identifier of an object of another type. In this article I will discuss how to solve this problem with TypeScript.


A bit of theory


TypeScript is based on structural typing, which fits well with the duck ideology of JavaScript. This is written enough articles. I will not repeat them, I will only indicate the main difference from nominative typing, which is more common in other languages. Let us examine a small example.


class Car { id: number; numberOfWheels: number; move (x: number, y: number) { //   } } class Boat { id: number; move (x: number, y: number) { //   } } let car: Car = new Boat(); //  TypeScript   let boat: Boat = new Car(); //        

Why does TypeScript behave that way? This is just a manifestation of structural typing. In contrast to the nominative, which monitors the type names, structural typing decides on the compatibility of types based on their content. The Car class contains all the properties and methods of the Boat class, so Car can be used as a Boat. The converse is not true, because as Boat, the numberOfWheels property is missing.


Typing identifiers


First, let's set the types for identifiers.


 type CarId: number; type BoatId: number; 

and rewrite the classes and using these types.


 class Car { id: CarId; numberOfWheels: number; move (x: number, y: number) { //   } } class Boat { id: BoatId; move (x: number, y: number) { //   } } 

You will notice that the situation has not changed much, because we still have no control over where we got the identifier, and you will be right. But this example already gives some advantages.


  1. In the process of developing a program, an identifier type can suddenly change. For example, a certain numerical car number, unique for a project, can be replaced with a string VIN number. Without specifying an identifier type, you will have to replace number with string in all places where it occurs. With the task of type, the change will need to be made only in one place where the type itself is defined.


  2. When calling functions, we get hints from our code editor, what type of identifiers should be. Suppose we have declared the following functions:


     function getCarById(id: CarId): Car { // ... } function getBoatById(id: BoatId): Boat { // ... } 

    Then we will receive a hint from the editor that we should convey not just a number, but CarId or BoatId.



We emulate the strictest typing


There is no nominal typing in TypeScript, but we can emulate its behavior by making any type unique. To do this, you need to add a unique property to the type. This technique is mentioned in the English-language articles under the term Branding, and here is how it looks:


 type BoatId = number & { _type: 'BoatId'}; type CarId = number & { _type: 'CarId'}; 

By pointing out that our types must simultaneously be both a number and an object with a property with a unique value, we made our types incompatible in understanding structural typing. Let's see how it works.


 let carId: CarId; let boatId: BoatId; let car: Car; let boat: Boat; car = getCarById(carId); // OK car = getCarById(boatId); // ERROR boat = getBoatById(boatId); // OK boat = getBoatById(carId); // ERROR carId = 1; // ERROR boatId = 2; // ERROR car = getCarById(3); // ERROR boat = getBoatById(4); // ERROR 

Everything looks good except for the last four lines. To create ids, you need a helper function:


 function makeCarIdFromVin(id: number): CarId { return vin as any; } 

The disadvantage of this method is that this function will remain in runtime.


We make strict typing a little less strict


In the last example, to create an identifier, I had to use an additional function. You can get rid of it using the Flavor interface definition:


 interface Flavoring<FlavorT> { _type?: FlavorT; } export type Flavor<T, FlavorT> = T & Flavoring<FlavorT>; 

Now you can set types for identifiers as follows:


 type CarId = Flavor<number, “CarId”> type BoatId = Flavor<number, “BoatId”> 

Since the _type property is optional, an implicit conversion can be used:


 let boatId: BoatId = 5; // OK let carId: CarId = 3; // OK 

And we still can not confuse identifiers:


 let carId: CarId = boatId; // ERROR 

Which option to choose


Both options have the right to exist. Branding has the advantage when it is necessary to protect a variable from direct assignment. This is useful if a variable stores a string in some format, such as an absolute file path, a date, or an IP address. The helper function that deals with type conversion in this case can also perform validation and processing of input data. In other cases, it is more convenient to use Flavor.


Sources


  1. Starting point on stackoverflow.com
  2. Free interpretation of the article

')

Source: https://habr.com/ru/post/446768/


All Articles