Guerra TypeScript ou Enum Conquest

Antecedentes


Há meio ano, nossa empresa decidiu mudar para tecnologias mais novas e mais modernas. Para isso, foi formado um grupo de especialistas: determinar a pilha tecnológica, fazer uma ponte para o código Legacy com base nessa pilha e, finalmente, transferir alguns dos módulos antigos para novos trilhos. Eu tive sorte de entrar neste grupo. A base de código do cliente é de aproximadamente um milhão de linhas de código. Escolhemos o TypeScript como idioma. Eles decidiram criar um substrato da GUI no vue em conjunto com o vue-class-component e a IoC .

Mas a história não é sobre como nos livramos do legado do código, mas sobre um pequeno incidente que resultou em uma verdadeira guerra de conhecimento. Quem se importa, bem-vindo ao gato.

Conhecendo o problema


Alguns meses após o início, o grupo de trabalho se acostumou com a nova pilha e conseguiu transferir parte do código antigo para ela. Você poderia até dizer que havia uma massa crítica de carne quando precisava parar, respirar fundo e ver o que havíamos feito.

Lugares que exigiam estudo profundo, como você sabe, eram suficientes. Mas de todas as coisas importantes, ironicamente, nada me pegou. Não como desenvolvedor. Mas um dos sem importância, pelo contrário, era assustador. Fiquei irritado ao ponto de loucura como trabalhamos com dados de enumeração. Não houve generalização. Você encontrará uma aula separada com um conjunto de métodos necessários e, em seguida, encontrará duas aulas para o mesmo, ou até algo misterioso e mágico. E não há ninguém para culpar. O ritmo que tomamos para nos livrar do legado foi muito grande.

Tendo levantado o tema das transferências entre colegas, recebi apoio. Aconteceu que não apenas eu não estava satisfeito com a falta de uma abordagem unificada para trabalhar com eles. Naquele momento, pareceu-me que, em algumas horas de codificação, atingiria o resultado desejado e me ofereceria para corrigir a situação. Mas como eu estava errado então ...

//    ,        . 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 onde começar? Apenas uma coisa veio à mente: tome como base uma enumeração semelhante a Java. Mas como eu queria me mostrar para meus colegas, decidi abandonar a herança clássica. Use um decorador. Além disso, o decorador pode ser aplicado com argumentos para fornecer às enumerações a funcionalidade necessária de maneira fácil e natural. A codificação não demorou muito tempo e, depois de algumas horas, eu já tinha algo parecido com isto:

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

E aqui eu sofri o primeiro fracasso. Acontece que você não pode alterar o tipo com a ajuda de um decorador. A Microsoft ainda tem um apelo sobre este assunto: Mutação no Decorador de Classes . Quando digo que você não pode alterar o tipo, quero dizer que seu IDE não saberá nada sobre isso e não oferecerá nenhuma dica e o preenchimento automático adequado. E você pode mudar o tipo o quanto quiser, apenas o bem ...

2. Herança


Como não tentei me convencer, tive que voltar à ideia de criar transferências baseadas na classe geral. Sim, e o que há de errado nisso? Fiquei irritado comigo mesmo. O tempo está acabando, pessoal do grupo, Deus o livre, estou gastando tempo com decoradores. Foi possível arquivar enum em geral em uma hora e seguir em frente. Que assim seja. Jogou rapidamente o código da classe base Enumerable e suspirou, sentindo alívio. Joguei o rascunho no repositório geral e pedi ao meu colega para verificar a solução.

Enumerável
 // :     ,  -     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; } } 

Mas a tragicomédia estava ganhando força. A versão 2.6.2 do TypeScript foi instalada na minha máquina, a versão na qual houve um erro inestimável. Inestimável, porque não é um bug, mas um fitcha. Uma voz da sala ao lado gritou que ele não faria nada. Erro ao compilar ( transpilar ). Eu não podia acreditar nos meus próprios ouvidos, pois sempre montava um projeto antes de um empurrão, mesmo que fosse um rascunho. E a voz interior sussurrou: isso é um fiasco, mano.

Após um breve teste, percebi que era uma versão TypeScript. Aconteceu que, se o nome genérico da classe coincidisse com o nome do genérico especificado no método estático, o compilador consideraria isso como um tipo. Mas, seja o que for, agora já faz parte da história dessa guerra pelo conhecimento do TypeScript.

A linha inferior: o problema com as transferências como era e permaneceu. Minha tristeza ...

Nota: não posso reproduzir esse comportamento agora mesmo com o 2.6.2, talvez tenha cometido um erro com a versão ou não tenha adicionado algo nos casos de teste. E a solicitação para o problema acima Permitir que membros estáticos façam referência a parâmetros de tipo de classe foi rejeitada.

3. Função de fundição


Apesar de haver uma solução distorcida, com uma indicação explícita do tipo da classe de enumeração nos métodos estáticos, por exemplo, State.valueOf <State> (), ela não se adequava a ninguém e, antes de tudo, a mim. Por um tempo, eu até deixei de lado a porra das transferências e perdi a confiança de que geralmente poderia resolver esse problema.

Depois de recuperar minha força moral, pesquisei na Internet por truques TypeScript, observei quem estava sofrendo com o quê, li a documentação da linguagem novamente, por precaução, e decidi, para evitá-la, terminar o trabalho. Sete horas de experimentos contínuos, sem procurar nada, nem mesmo café, deram o resultado. Apenas uma função, consistindo em uma linha de código, coloca tudo em seu 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; } 

E a declaração de uma enumeração semelhante a Java agora se parece com isso:

 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 

Não sem curiosidade, com uma importação extra de IStaticEnum, que não é usada em nenhum lugar (veja o exemplo acima). Na versão muito ruim do TypeScript 2.6.2, você precisa especificá-lo explicitamente. Um erro no tópico aqui .

Total


Se você sofre por muito tempo, algo vai dar certo. Link para o github com o resultado do trabalho realizado aqui . Para mim, descobri que o TypeScript é uma linguagem com ótimos recursos. Existem tantas oportunidades que você pode se afogar nelas. E quem não quer afundar, aprende a nadar. Se você retornar ao tópico de transferências, poderá ver como os outros trabalham com eles:


Escreva sobre o seu trabalho, acho que a comunidade estará interessada. Obrigado a todos por sua paciência e interesse.

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


All Articles