📜 ⬆️ ⬇️

War of TypeScript or Conquest of Enum

Prehistory


Half a year ago, our company decided to switch to newer and more modern technologies. To do this, they formed a group of specialists who had to: decide on the technological stack, make a bridge to the legacy code on the basis of this stack, and finally, transfer part of the old modules to new rails. I was lucky to get into this group. The client code base is approximately one million lines of code. We chose TypeScript as the language. GUI decided to make the substrate on the vue, coupled with the vue-class-component and IoC .

But the story is not about how we got rid of Legacy code, but about one small incident that resulted in a real war of knowledge. Who cares, welcome under cat.

Getting to know the problem


A couple of months after the start, the working group got used to the new stack well and already managed to transfer part of the old code to it. One could even say that a certain critical mass of meat had accumulated, when it was necessary to stop, take a breath, and take a look at what we wrapped up.

There were enough places that required deep study, as you understand. But of all the important things, ironically, nothing clung to me. Not clinging as a developer. But one of the unimportant, on the contrary, did not give rest. I was completely annoyed by the way we work with data like an enumeration. There was no generalization. Then you will meet a separate class with a set of required methods, then you will find two classes for the same, or even something mysterious and magical. And there is nobody to blame here. The pace that we took to get rid of Legacy was too great.
')
Raising the topic with transfers among colleagues, I received support. It turned out that not only I am not satisfied with the lack of a unified approach to working with them. At that moment, I had a dream that in a couple of hours of coding I would achieve the desired result and volunteered to correct the situation. But how was I wrong then ...

//    ,        . import {Enum} from "ts-jenum"; @Enum("text") export class State { static readonly NEW = new State("New"); static readonly ACTIVE = new State("Active"); static readonly BLOCKED = new State("Blocked"); private constructor(public text: string) { super(); } } //   console.log("" + State.ACTIVE); // Active console.log("" + State.BLOCKED); // Blocked console.log(State.values()); // [State.NEW, State.ACTIVE, State.BLOCKED] console.log(State.valueOf("New")); // State.NEW console.log(State.valueByName("NEW")); // State.NEW console.log(State.ACTIVE.enumName); // ACTIVE 

1. Decorator


Where to begin? Only one thing came to mind: to base Java-like enumeration. But since I wanted to show off in front of my colleagues, I decided to abandon the classical inheritance. Use decorator instead. The decorator, moreover, could be applied with arguments in order to give the enumeration the required functionality easily and naturally. Coding did not take much time and after a couple of hours I already had something similar to this:

Decorator
 export function Enum(idProperty?: string) { // tslint:disable-next-line return function <T extends Function, V>(target: T): T { if ((target as any).__enumMap__ || (target as any).__enumValues__) { const enumName = (target as any).prototype.constructor.name; throw new Error(`The enumeration ${enumName} has already initialized`); } const enumMap: any = {}; const enumMapByName: any = {}; const enumValues = []; // Lookup static fields for (const key of Object.keys(target)) { const value: any = (target as any)[key]; // Check static field: to be instance of enum type if (value instanceof target) { let id; if (idProperty) { id = (value as any)[idProperty]; if (typeof id !== "string" && typeof id !== "number") { const enumName = (target as any).prototype.constructor.name; throw new Error(`The value of the ${idProperty} property in the enumeration element ${enumName}. ${key} is not a string or a number: ${id}`); } } else { id = key; } if (enumMap[id]) { const enumName = (target as any).prototype.constructor.name; throw new Error(`An element with the identifier ${id}: ${enumName}.${enumMap[id].enumName} already exists in the enumeration ${enumName}`); } enumMap[id] = value; enumMapByName[key] = value; enumValues.push(value); Object.defineProperty(value, "__enumName__", {value: key}); Object.freeze(value); } } Object.freeze(enumMap); Object.freeze(enumValues); Object.defineProperty(target, "__enumMap__", {value: enumMap}); Object.defineProperty(target, "__enumMapByName__", {value: enumMapByName}); Object.defineProperty(target, "__enumValues__", {value: enumValues}); if (idProperty) { Object.defineProperty(target, "__idPropertyName__", {value: idProperty}); } //  values(), valueOf     ,    -. Object.freeze(target); return target; }; } 

And here I suffered a first failure. It turned out that using the decorator can not change the type. On this subject, Microsoft even has a message: Class Decorator Mutation . When I say that you cannot change the type, I mean that your IDE will not know anything about it and will not offer any prompts or adequate auto-completion. And you can change the type as much as you like, just to sense from this ...

2. Inheritance


As I did not try to persuade myself, but I had to return to the idea of ​​creating transfers on the basis of a general class. And what's the big deal? I was annoyed with myself. Time goes on, guys from the group of figachat god forbid, and I spend time here for decorators. It was possible to cut down enum in an hour and move on. So be it. Quickly threw the code of the base class Enumerable and sighed, feeling relieved. I threw the draft in a common repository and asked a colleague to check the solution.

Enumerable
 // :     ,  -     export class Enumerable<T> { constructor() { const clazz = this.constructor as any as EnumStore; if (clazz.__enumMap__ || clazz.__enumValues__ || clazz.__enumMapByName__) { throw new Error(`It is forbidden to create ${clazz.name} enumeration elements outside the enumeration`); } } static values<T>(): ReadonlyArray<T> { const clazz = this as any as EnumStore; if (!clazz.__enumValues__) { throw new Error(`${clazz.name} enumeration has not been initialized. It is necessary to add the decorator @Enum to the class`); } return clazz.__enumValues__; } static valueOf<T>(id: string | number): T { const clazz = this as any as EnumStore; if (!clazz.__enumMap__) { throw new Error(`${clazz.name} enumeration has not been initialized. It is necessary to add the decorator @Enum to the class`); } const value = clazz.__enumMap__[id]; if (!value) { throw new Error(`The element with ${id} identifier does not exist in the $ {clazz.name} enumeration`); } return value; } static valueByName<T>(name: string): T { const clazz = this as any as EnumStore; if (!clazz.__enumMapByName__) { throw new Error(`${clazz.name} enumeration has not been initialized. It is necessary to add the decorator @Enum to the class`); } const value = clazz.__enumMapByName__[name]; if (!value) { throw new Error(`The element with ${name} name does not exist in the ${clazz.name} enumeration`); } return value; } get enumName(): string { return (this as any).__enumName__; } toString(): string { const clazz = this.constructor as any as EnumStore; if (clazz.__idPropertyName__) { const self = this as any; return self[clazz.__idPropertyName__]; } return this.enumName; } } 

But tragicomedy was gaining its full speed. I had TypeScript version 2.6.2 installed on my machine, exactly the version in which there was an invaluable bug. Invaluable, because not a bug, but a fitch. A voice from the next room shouted that he was not going to do anything. Compile error ( transpilation ). I did not believe my own ears, for I always collect a project before pushing, even if it is a draft. And the inner voice whispered: it's a fiasco, bro.

After a brief trial, I realized that it was the TypeScript version. It turned out that if the generic name of the class coincided with the name of the generic specified in the static method, then the compiler considered this as one type. But no matter how it was there, now it is already part of the history of the war for the knowledge of TypeScript.

The bottom line: the problem with transfers as it was and has remained. My sadness ...

Note: I can not reproduce this behavior in my own now with 2.6.2, it is possible that the version was mistaken or did not add something in test examples. And the request for the problem described above has been rejected.

3. Casting function


Despite the fact that there was a curved solution, with an explicit indication of the type of the enumeration class in static methods, for example, State.valueOf <State> (), it did not suit anyone and, above all, me. For a while, I even put aside the fucking transfers and lost confidence that I could solve this problem.

Morally recuperating, I searched the Internet for tricks with TypeScript, looked at who was suffering from what, read the documentation on the language again, just in case, and decided, in order not to become, to finish the job. Seven hours of continuous experiments, not looking at anything, even on coffee, gave their result. Only one function, consisting of one line of code, put everything in its place.

 export function EnumType<T>(): IStaticEnum<T> { return (<IStaticEnum<T>> Enumerable); } //  IStaticEnum : export interface IStaticEnum<T> { new(): {enumName: string}; values(): ReadonlyArray<T>; valueOf(id: string | number): T; valueByName(name: string): T; } 

And the declaration of a Java-like enumeration itself now looks like this:

 import {Enum, EnumType, IStaticEnum} from "ts-jenum"; @Enum("text") export class State extends EnumType<State>() { static readonly NEW = new State("New"); static readonly ACTIVE = new State("Active"); static readonly BLOCKED = new State("Blocked"); private constructor(public text: string) { super(); } } //   console.log("" + State.ACTIVE); // Active console.log("" + State.BLOCKED); // Blocked console.log(State.values()); // [State.NEW, State.ACTIVE, State.BLOCKED] console.log(State.valueOf("New")); // State.NEW console.log(State.valueByName("NEW")); // State.NEW console.log(State.ACTIVE.enumName); // ACTIVE 

Not without a curiosity, with the extra import of IStaticEnum, which is not used anywhere (see the example above). In the same ill-fated version of TypeScript 2.6.2 you need to specify it explicitly. The bug on the topic is here .

Total


If you suffer for a long time, something will turn out. Link to githab with the result of the work done here . For myself, I discovered that TypeScript is a language with great potential. There are so many of these possibilities that you can drown in them. And who does not want to go to the bottom, learns to swim. If you go back to the topic of transfers, you can see how others work with them:


Write about your work, I think the community will be interested. Thank you all for your patience and interest.

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


All Articles