Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can typescript external modules have circular dependencies?

It looks like this is not allowed. requireJS is throwing an error on the following (this post is different as it was resolved with internal modules):

element.ts:

import runProperties = require('./run-properties');
export class Element {
   public static factory (element : IElement) : Element {

        switch (element.type) {
            case TYPE.RUN_PROPERTIES :
                return new runProperties.RunProperties().deserialize(<runProperties.IRunProperties>element);
        }
        return null;
    }
}

run-properties.ts:

import element = require('./element');

export class RunProperties extends element.Element implements IRunProperties {
}
like image 959
David Thielen Avatar asked Apr 24 '14 20:04

David Thielen


People also ask

Is it okay to have circular dependency?

Circular dependencies also make code difficult to read and maintain over time, which opens the door to error-prone applications that are difficult to test. If circular dependencies proliferate an architecture, any changes to a single module will likely cause a large ripple effect of errors for others.

What is circular dependency in typescript?

In software engineering, a circular dependency is a relation between two or more modules which either directly or indirectly depend on each other to function properly. Such modules are also known as mutually recursive.

How can circular dependencies be avoided?

Circular dependencies can be introduced when implementing callback functionality. This can be avoided by applying design patterns like the observer pattern.

What causes circular dependency?

A circular dependency occurs when two classes depend on each other. For example, class A needs class B, and class B also needs class A. Circular dependencies can arise in Nest between modules and between providers. While circular dependencies should be avoided where possible, you can't always do so.


2 Answers

No, modules can't have circular dependencies unless they are in the same file. Each file is being processed in sequence, synchronously, so the full file definition (including all of the exports for example) hasn't been completed when it goes to second file, which immediately tries to require/reference the first file, and so on.

Normally, you can break a circular dependency by introducing an interface or base class into a common definition file(s) (basically interfaces only) and having the other files use that as a common "interface" rather than directly referencing the classes. This is a typical pattern in many platforms.

like image 163
WiredPrairie Avatar answered Oct 28 '22 09:10

WiredPrairie


I have same issue, I was able to fix it by creating factory class that allows registration of child classes and used Generics for instantiation.

Reference: https://www.typescriptlang.org/docs/handbook/generics.html#using-class-types-in-generics

See sample code below:

Base Class (abstract.control.ts)

export type AbstracControlOptions = {
    key?:string;
}
export abstract class AbstractControl {
    key:string;
    constructor(options:AbstracControlOptions){
        this.key = options.key;
    }    
}

Parent Class (container.ts)

import { AbstractControl, AbstracControlOptions } from './abstract.control';
import { Factory } from './factory';

export { AbstracControlOptions };
export abstract class Container extends AbstractControl {
    children: AbstractControl[] = [];
    constructor(options: AbstracControlOptions) {
        super(options);
    }
    addChild(options: { type: string }) {
        var Control:any = Factory.ControlMap[options.type];
        if (Control) {
            this.children.push(Factory.create(Control, options));
        }
    }
}

I don't have to import the child classes any more, because I'm using factory.ts to instantiate the child classes.

Factory Class(factory.ts)

import {AbstractControl, AbstracControlOptions} from './abstract.control';

type ControlMap<T extends AbstractControl> = {
    [type:string]:T
};

export class Factory{
    static ControlMap: ControlMap<any> = {};
    static create<T extends AbstractControl>(c: { new ({}): T; }, options: AbstracControlOptions): T {
        return new c(options);
    } 
}

Although class constructor seems to be called at c: { new ({}): T } but it does not actually calls it. But gets the reference to the constructor via new operator. The parameter {} to the constructor in my case is required because the base class AbstractControl requires it.

(1) Child Class(layout.ts)

import { Factory } from './factory';
import { Container, AbstracControlOptions } from './container';

export type LayoutlOptions = AbstracControlOptions & {
    type:"layout";
}
export class Layout extends Container {
    type: string = "layout";
    constructor(options:LayoutlOptions) {
        super(options);
    }
}
Factory.ControlMap["layout"] = Layout;

(2) Child Class(repeater.ts)

import { Factory } from './factory'
import { Container, AbstracControlOptions } from './container';

export type RepeaterOptions = AbstracControlOptions & {
    type: "repeater";
}
export class Repeater extends Container {
    type: string = "repeater";
    constructor(options:RepeaterOptions) {
        super(options);
    }
}
Factory.ControlMap["repeater"] = Repeater;
like image 3
Lucman Abdulrachman Avatar answered Oct 28 '22 09:10

Lucman Abdulrachman