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


    After 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")
    exportclassState{
        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);        // Activeconsole.log("" + State.BLOCKED);       // Blockedconsole.log(State.values());           // [State.NEW, State.ACTIVE, State.BLOCKED]console.log(State.valueOf("New"));     // State.NEWconsole.log(State.valueByName("NEW")); // State.NEWconsole.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 required functionality to the enumerations 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-linereturnfunction <T extends Function, V>(target: T): T {
            if ((target asany).__enumMap__ || (target asany).__enumValues__) {
                const enumName = (target asany).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 ofObject.keys(target)) {
                const value: any = (target asany)[key];
                // Check static field: to be instance of enum typeif (value instanceof target) {
                    let id;
                    if (idProperty) {
                        id = (valueasany)[idProperty];
                        if (typeof id !== "string" && typeof id !== "number") {
                            const enumName = (target asany).prototype.constructor.name;
                            throw new Error(`The valueof the ${idProperty} property in the enumeration element ${enumName}. ${key} isnot a string or a number: ${id}`);
                        }
                    } else {
                        id = key;
                    }
                    if (enumMap[id]) {
                        const enumName = (target asany).prototype.constructor.name;
                        throw new Error(`An element with the identifier ${id}: ${enumName}.${enumMap[id].enumName} already existsin 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 asanyas EnumStore;
            if (clazz.__enumMap__ || clazz.__enumValues__ || clazz.__enumMapByName__) {
                throw new Error(`It is forbidden tocreate ${clazz.name} enumeration elements outside the enumeration`);
            }
        }
        static values<T>(): ReadonlyArray<T> {
            const clazz = this asanyas EnumStore;
            if (!clazz.__enumValues__) {
                throw new Error(`${clazz.name} enumeration has not been initialized. It is necessary toadd the decorator @Enum to the class`);
            }
            return clazz.__enumValues__;
        }
        static valueOf<T>(id: string | number): T {
            const clazz = this asanyas EnumStore;
            if (!clazz.__enumMap__) {
                throw new Error(`${clazz.name} enumeration has not been initialized. It is necessary toadd 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`);
            }
            returnvalue;
        }
        static valueByName<T>(name: string): T {
            const clazz = this asanyas EnumStore;
            if (!clazz.__enumMapByName__) {
                throw new Error(`${clazz.name} enumeration has not been initialized. It is necessary toadd 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`);
            }
            returnvalue;
        }
        get enumName(): string {
            return (this asany).__enumName__;
        }
        toString(): string {
            const clazz = this.constructor asanyas EnumStore;
            if (clazz.__idPropertyName__) {
                const self = this asany;
                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. Priceless, 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, 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, maybe with the version I was mistaken or did not add something in the test examples. And the request for the above problem Allow was 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 TypeScript tricks, looked at who was suffering from what, read the language documentation once 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 results. Only one function, consisting of one line of code, put everything in its place.

    export functionEnumType<T>(): IStaticEnum<T> {
        return (<IStaticEnum<T>> Enumerable);
    }
    // где IStaticEnum это:
    export interfaceIStaticEnum<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")
    exportclassStateextendsEnumType<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);        // Activeconsole.log("" + State.BLOCKED);       // Blockedconsole.log(State.values());           // [State.NEW, State.ACTIVE, State.BLOCKED]console.log(State.valueOf("New"));     // State.NEWconsole.log(State.valueByName("NEW")); // State.NEWconsole.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.

    Also popular now: