Texto datilografado - dependência circular do decorador

Sep 18 2020

Recebo uma dependência circular dos meus decoradores porque minha classe ThingAtem uma relação com ThingBe vice-versa. Eu li várias perguntas sobre esse problema:

  • Bela correção para problema de dependências circulares em Javascript / Typescript

  • Decoradores e dependências circulares do TypeScript

Mas não consegui encontrar uma solução válida para o meu caso. Eu tentei como muitas pessoas sugerem mudar de @hasOne(ThingA)para @hasOne(() => ThingA)para forçar um carregamento lento e quebrar a dependência, mas essa solução não funciona porque não consigo obter o nome do construtor. Preciso do nome do construtor (exemplo: 'ThingA') para adicioná-lo aos metadados do construtor ThingB.

Seguindo meu código original (sem modificação de lazyload)

ThingA

@hasAtLeast(0, ThingB)
export class ThingA extends RTContent {
    @IsEmail()
    email: string;
}

ThingB

@hasOne(ThingA)
export class ThingB extends RTContent {
    @IsString()
    description: string;
}

Decoradores:

export type LinkConstraint<C extends RTContent> = {
    content: string; // name of the constructor (ex. 'ThingA')
    maxOccurrences: number;
    minOccurrences: number;
    constructor: { new(...args: any[]): C };
}

function constraintFactory<C extends RTContent>(minOccurrences: number, maxOccurrences: number, associated: { new(...args: any[]): C }) {
    return (constructor: Function) => {
        const constraints = Reflect.getMetadata('linkConstraints', constructor) || [];
        const constraint: LinkConstraint<C> = {
            content: associated?.name,
            minOccurrences,
            maxOccurrences,
            constructor: associated
        };
        constraints.push(constraint);
        Reflect.defineMetadata('linkConstraints', constraints, constructor)
    }
}

export function hasOne<C extends RTContent>(associated: { new(...args: any[]): C }) {
    return constraintFactory(1, 1, associated)
}

export function hasAtLeast<C extends RTContent>(minOccurrences: number, associated: { new(...args: any[]): C }) {
    return constraintFactory(minOccurrences, Infinity, associated)
}

Respostas

2 hackape Sep 26 2020 at 14:26

Vejo que seu decorador não modifica realmente o construtor, ele apenas executa algum código de efeito colateral para adicionar alguma entrada de metadados. Portanto, a @decoratorsintaxe não é obrigatória.

Meu conselho é que você não decore ThingAnem ThingB, apenas exporte-os como estão. Você adia a decoração em outro módulo, que deve ser o pai comum de ambos ThingAe ThingB. Desta forma, a dependência circular é resolvida.

Por exemplo, em './things/index.ts'você:

import { ThingA } from './things/ThingA';
import { ThingB } from './things/ThingB';

hasOne(ThingA)(ThingB);
hasAtLeast(0, ThingB)(ThingA);

export { ThingA, ThingB }

Agora outra parte do seu código pode importar de './things/index.ts', em vez de diretamente de './things/ThingA(or B).ts'. Isso garantiria que a decoração fosse executada antes da instanciação das classes.


Se você deve usar o decorador, o carregamento lento é sua melhor aposta. @hasOne(() => ThingA)deve fazer o truque, mas você precisa modificar a implementação de hasOneum pequeno hack.

function hasOne(target) {
  if (typeof target === "function") {
    setTimeout(() => {
      _workOnTarget(target())
    }, 0)
  } else {
    _workOnTarget(target)
  }
}

A chave é atrasar o acesso ao valor da variável.

Para que esse hack funcione, ainda contamos com o fato de que esses decoradores são apenas efeitos colaterais, não modifique o construtor. Portanto, esta NÃO é uma solução geral para o problema de dependência circular. O padrão mais geral é a avaliação preguiçosa do curso. Mais complicado, porém, se você realmente precisar, por favor, pergunte nos comentários.

Para o seu caso, o impl acima deve funcionar. Mas você não deve instanciar ThingAou B dentro do nível superior de qualquer módulo, porque isso aconteceria antes do setTimeoutretorno de chamada, interrompendo o hack.