TypeScript War o Enum Conquest

Antecedentes


Hace medio a帽o, nuestra compa帽铆a decidi贸 cambiar a tecnolog铆as m谩s modernas y de moda. Para hacer esto, se form贸 un grupo de especialistas, que deb铆a: decidir sobre la pila tecnol贸gica, hacer un puente al c贸digo heredado sobre la base de esta pila y finalmente transferir algunos de los m贸dulos antiguos a nuevos rieles. Tuve la suerte de entrar en este grupo. La base del c贸digo del cliente es aproximadamente un mill贸n de l铆neas de c贸digo. Elegimos TypeScript como el lenguaje. Decidieron hacer un sustrato GUI en vue junto con vue-class-component e IoC .

Pero la historia no se trata de c贸mo nos deshicimos del legado del c贸digo, sino de un peque帽o incidente que result贸 en una verdadera guerra de conocimientos. A qui茅n le importa, bienvenido al gato.

Conociendo el problema


Un par de meses despu茅s del inicio, el grupo de trabajo se sinti贸 c贸modo con la nueva pila y logr贸 transferirle parte del c贸digo anterior. Incluso podr铆a decir que hubo una masa cr铆tica de carne cuando necesitaba detenerse, respirar y ver lo que hab铆amos hecho.

Los lugares que requieren un estudio profundo, como saben, fueron suficientes. Pero de todas las cosas importantes, ir贸nicamente, nada me atrap贸. No como desarrollador. Pero uno de los sin importancia, por el contrario, era inquietante. Me molest贸 hasta el punto de la locura c贸mo trabajamos con los datos de enumeraci贸n. No hubo generalizaci贸n. Te encontrar谩s con una clase separada con un conjunto de m茅todos requeridos, luego encontrar谩s dos clases, para la misma, o incluso algo misterioso y m谩gico. Y no hay nadie a quien culpar. El ritmo que tomamos para deshacernos del legado fue demasiado grande.

Habiendo planteado el tema de las transferencias entre colegas, recib铆 apoyo. Result贸 que no solo no estaba satisfecho con la falta de un enfoque unificado para trabajar con ellos. En ese momento, me pareci贸 que en un par de horas de codificaci贸n lograr铆a el resultado deseado y me ofrec铆 para corregir la situaci贸n. Pero qu茅 equivocado estaba entonces ...

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


Por donde empezar Solo se me ocurri贸 una cosa: tomar como base una enumeraci贸n similar a Java. Pero como quer铆a presumir ante mis colegas, decid铆 abandonar la herencia cl谩sica. Use un decorador en su lugar. Adem谩s, el decorador podr铆a aplicarse con argumentos para dar a las enumeraciones la funcionalidad requerida de manera f谩cil y natural. La codificaci贸n no tom贸 mucho tiempo, y despu茅s de un par de horas ya ten铆a algo similar a esto:

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

Y aqu铆 sufr铆 el primer fracaso. Result贸 que no puede cambiar el tipo con la ayuda de un decorador. Microsoft incluso tiene una apelaci贸n sobre este tema: Class Decorator Mutation . Cuando digo que no puede cambiar el tipo, me refiero a que su IDE no sabr谩 nada sobre esto y no ofrecer谩 ninguna pista y autocompletado adecuado. Y puedes cambiar el tipo tanto como quieras, solo lo bueno ...

2. Herencia


Como no trat茅 de persuadirme, tuve que volver a la idea de crear transferencias basadas en la clase general. S铆, y 驴qu茅 hay de malo en eso? Estaba molesto por m铆 mismo. Se est谩 acabando el tiempo, muchachos del grupo, Dios no lo quiera, estoy pasando tiempo en decoradores. Fue posible presentar una enumeraci贸n en general en una hora y seguir adelante. Que as铆 sea. Lanz贸 r谩pidamente el c贸digo de la clase base Enumerable y suspir贸, sintiendo alivio. Lanc茅 el borrador al repositorio general y le ped铆 a mi colega que verificara la soluci贸n.

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

Pero la tragicomedia estaba ganando fuerza. La versi贸n 2.6.2 de TypeScript se instal贸 en mi m谩quina, la versi贸n en la que hab铆a un error invaluable. No tiene precio, porque no es un error, sino un fitcha. Una voz de la habitaci贸n contigua grit贸 que no iba a hacer nada. Error al compilar ( transpilar ). No pod铆a creer lo que escuchaba, porque siempre armaba un proyecto antes de un impulso, incluso si era un borrador. Y la voz interior susurr贸: esto es un fiasco, hermano.

Despu茅s de una breve prueba, me di cuenta de que era una versi贸n TypeScript. Result贸 que si el nombre gen茅rico de la clase coincid铆a con el nombre del gen茅rico especificado en el m茅todo est谩tico, entonces el compilador consideraba esto como un tipo. Pero sea lo que sea, ahora ya es parte de la historia de esa guerra por el conocimiento de TypeScript.

El resultado final: el problema con las transferencias como era y se mantuvo. Mi pena ...

Nota: No puedo reproducir este comportamiento ahora mismo con 2.6.2, tal vez comet铆 un error con la versi贸n o no agregu茅 algo en los casos de prueba. Y se rechaz贸 la solicitud del problema anterior Permitir que los miembros est谩ticos hagan referencia a par谩metros de tipo de clase .

3. Funci贸n de fundici贸n


A pesar del hecho de que hab铆a una soluci贸n torcida, con una indicaci贸n expl铆cita del tipo de la clase de enumeraci贸n en m茅todos est谩ticos, por ejemplo, State.valueOf <State> (), no se adaptaba a nadie y, en primer lugar, a m铆. Por un tiempo, incluso dej茅 de lado las jodidas transferencias y perd铆 la confianza de que generalmente podr铆a resolver este problema.

Una vez que recuper茅 mi fortaleza moral, busqu茅 en Internet trucos de TypeScript, mir茅 qui茅n sufr铆a de qu茅, le铆 la documentaci贸n del idioma nuevamente, por si acaso, y decid铆, para evitarlo, terminar el trabajo. Siete horas de experimentos continuos, sin buscar nada, ni siquiera caf茅, dieron su resultado. Solo una funci贸n, que consiste en una l铆nea de c贸digo, coloca todo en su lugar.

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

Y la declaraci贸n de una enumeraci贸n similar a Java ahora se ve as铆:

 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 

No sin curiosidad, con una importaci贸n adicional de IStaticEnum, que no se usa en ning煤n lado (ver el ejemplo anterior). En la versi贸n muy desafortunada de TypeScript 2.6.2, debe especificarlo expl铆citamente. Un error sobre el tema aqu铆 .

Total


Si sufres por mucho tiempo, algo funcionar谩. Enlace a github con el resultado del trabajo realizado aqu铆 . Por m铆 mismo, descubr铆 que TypeScript es un lenguaje con excelentes caracter铆sticas. Hay tantas de estas oportunidades que puede ahogarse en ellas. Y quien no quiere hundirse, aprende a nadar. Si vuelve al tema de las transferencias, puede ver c贸mo otros trabajan con ellas:


Escribe sobre tu trabajo, creo que la comunidad estar谩 interesada. Gracias a todos por su paciencia e inter茅s.

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


All Articles