TypeScript War oder Enum Conquest

Hintergrund


Vor einem halben Jahr hat unser Unternehmen beschlossen, auf neuere und modischere Technologien umzusteigen. Zu diesem Zweck wurde eine Gruppe von Spezialisten gebildet, die: sich für den technologischen Stack entscheiden, auf der Grundlage dieses Stacks eine Brücke zum Legacy-Code schlagen und schließlich einige der alten Module auf neue Schienen übertragen sollten. Ich hatte das Glück, in diese Gruppe zu kommen. Die Client-Codebasis besteht aus ungefähr einer Million Codezeilen. Wir haben TypeScript als Sprache gewählt. Sie beschlossen, ein GUI-Substrat auf vue in Verbindung mit vue-class-component und IoC zu erstellen .

Aber die Geschichte handelt nicht davon, wie wir das Erbe des Codes losgeworden sind, sondern von einem kleinen Vorfall, der zu einem echten Wissenskrieg geführt hat. Wen kümmert es, willkommen bei Katze.

Das Problem kennenlernen


Einige Monate nach dem Start machte sich die Arbeitsgruppe mit dem neuen Stapel vertraut und schaffte es, einen Teil des alten Codes darauf zu übertragen. Man könnte sogar sagen, dass es eine kritische Masse an Fleisch gab, wenn man anhalten, Luft holen und sich ansehen musste, was wir getan hatten.

Wie Sie wissen, waren Orte, die ein gründliches Studium erforderten, ausreichend. Aber von all den wichtigen Dingen hat mich ironischerweise nichts erwischt. Nicht als Entwickler. Aber einer der unwichtigen, im Gegenteil, verfolgte. Ich war bis zum Wahnsinn genervt, wie wir mit Aufzählungsdaten arbeiten. Es gab keine Verallgemeinerung. Sie werden eine separate Klasse mit einer Reihe erforderlicher Methoden treffen, dann werden Sie zwei Klassen für dieselbe oder sogar etwas Geheimnisvolles und Magisches finden. Und es ist niemand schuld. Das Tempo, mit dem wir das Erbe loswurden, war zu groß.

Nachdem ich das Thema Transfers unter Kollegen angesprochen hatte, erhielt ich Unterstützung. Es stellte sich heraus, dass nicht nur ich mit dem Fehlen eines einheitlichen Ansatzes für die Arbeit mit ihnen nicht zufrieden war. In diesem Moment schien es mir, dass ich in ein paar Stunden Codierung das gewünschte Ergebnis erzielen würde und mich freiwillig bereit erklärte, die Situation zu korrigieren. Aber wie falsch ich damals war ...

//    ,        . 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. Dekorateur


Wo soll ich anfangen? Nur eines kam mir in den Sinn: Nehmen Sie eine Java-ähnliche Aufzählung als Grundlage. Aber da ich mich meinen Kollegen zeigen wollte, beschloss ich, das klassische Erbe aufzugeben. Verwenden Sie stattdessen einen Dekorateur. Darüber hinaus kann der Dekorateur mit Argumenten versehen werden, um den Aufzählungen einfach und natürlich die erforderliche Funktionalität zu verleihen. Das Codieren dauerte nicht lange und nach ein paar Stunden hatte ich schon etwas Ähnliches:

Dekorateur
 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; }; } 

Und hier habe ich den ersten Misserfolg erlitten. Es stellte sich heraus, dass Sie den Typ nicht mit Hilfe eines Dekorateurs ändern können. Microsoft hat sogar einen Appell zu diesem Thema: Class Decorator Mutation . Wenn ich sage, dass Sie den Typ nicht ändern können, bedeutet dies, dass Ihre IDE nichts darüber weiß und keine Hinweise und eine angemessene automatische Vervollständigung bietet. Und Sie können den Typ so oft ändern, wie Sie möchten, nur das Gute daran ...

2. Vererbung


Da ich nicht versuchte, mich selbst zu überzeugen, musste ich zu der Idee zurückkehren, Transfers basierend auf der allgemeinen Klasse zu erstellen. Ja, und was ist daran falsch? Ich war von mir selbst genervt. Die Zeit läuft davon, Leute aus der Gruppe, Gott bewahre, ich verbringe Zeit mit Dekorateuren. Es war möglich, die Aufzählung im Allgemeinen in einer Stunde einzureichen und fortzufahren. So sei es. Warf schnell den Code der Basisklasse Enumerable und seufzte und fühlte sich erleichtert. Ich warf den Entwurf in das allgemeine Repository und bat meinen Kollegen, die Lösung zu überprüfen.

Aufzählbar
 // :     ,  -     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; } } 

Aber die Tragikomödie gewann an Fahrt. TypeScript Version 2.6.2 wurde auf meinem Computer installiert, die Version, in der es einen unschätzbaren Fehler gab. Unbezahlbar, denn es ist kein Bug, sondern eine Fitcha. Eine Stimme aus dem Nebenzimmer rief, dass er nichts tun würde. Fehler beim Kompilieren ( Transpilieren ). Ich konnte meinen eigenen Ohren nicht trauen, denn ich habe immer vor einem Push ein Projekt zusammengestellt, auch wenn es ein Entwurf war. Und die innere Stimme flüsterte: Das ist ein Fiasko, Bruder.

Nach einem kurzen Test stellte ich fest, dass es sich um eine TypeScript-Version handelte. Es stellte sich heraus, dass der Compiler dies als einen Typ betrachtete, wenn der generische Name der Klasse mit dem Namen des in der statischen Methode angegebenen generischen Namens übereinstimmte. Aber was auch immer es war, jetzt ist es für die Kenntnis von TypeScript bereits Teil der Geschichte dieses Krieges.

Fazit: Das Problem mit Transfers wie es war und blieb. Meine Trauer ...

Hinweis: Ich kann dieses Verhalten jetzt mit 2.6.2 nicht selbst reproduzieren. Möglicherweise habe ich einen Fehler mit der Version gemacht oder in den Testfällen nichts hinzugefügt. Die Anforderung für das obige Problem " Statischen Mitgliedern erlauben, auf Parameter des Klassentyps zu verweisen" wurde abgelehnt.

3. Casting-Funktion


Trotz der Tatsache, dass es eine krumme Lösung gab, die explizit den Typ der Aufzählungsklasse in statischen Methoden angibt, z. B. State.valueOf <State> (), passte sie niemandem und vor allem mir. Für eine Weile legte ich sogar die verdammten Transfers beiseite und verlor das Vertrauen, dass ich dieses Problem generell lösen könnte.

Nachdem ich meine moralische Stärke wiedererlangt hatte, suchte ich im Internet nach TypeScript-Tricks, schaute, wer unter was litt, las die Sprachdokumentation für alle Fälle erneut und beschloss, den Job zu beenden, um dies zu vermeiden. Sieben Stunden ununterbrochener Experimente, bei denen selbst nach Kaffee nichts gesucht wurde, ergaben das Ergebnis. Nur eine Funktion, die aus einer Codezeile besteht, setzt alles an seinen Platz.

 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; } 

Und die Deklaration einer Java-ähnlichen Aufzählung sieht jetzt so aus:

 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 

Nicht ohne Neugierde, mit einem zusätzlichen Import von IStaticEnum, das nirgendwo verwendet wird (siehe obiges Beispiel). In der sehr unglücklichen Version von TypeScript 2.6.2 müssen Sie dies explizit angeben. Ein Fehler zum Thema hier .

Insgesamt


Wenn Sie lange leiden, wird etwas funktionieren. Link zu Github mit dem Ergebnis der hier geleisteten Arbeit. Ich selbst habe festgestellt, dass TypeScript eine Sprache mit großartigen Funktionen ist. Es gibt so viele dieser Möglichkeiten, dass Sie darin ertrinken können. Und wer nicht sinken will, lernt schwimmen. Wenn Sie zum Thema Überweisungen zurückkehren, können Sie sehen, wie andere mit ihnen arbeiten:


Schreiben Sie über Ihre Arbeit, ich denke, die Community wird interessiert sein. Vielen Dank für Ihre Geduld und Ihr Interesse.

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


All Articles