Texto datilografado - dependência circular do decorador
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
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.