Circular Dependencies

What are Circular Dependencies?

A circular dependency is when one of your modules imports another modules, which directly or via other modules imports the first module.

Examples: Direct reference: A -> B -> A

// a.js
import { b } from './b.js'

// b.js
import { a } from './a.js'

Indirect reference: A -> B -> C -> A

// a.ts
import { b } from './b.ts'

// b.ts
import { c } from './c.ts'

// c.ts
import { a } from './a.ts'

How do they affect us?

Not all circular dependencies cause issues. However you might have seen this quite vague exception message: TypeError: <class_or_module> expression must either be null or a function, not undefined.

Let’s explain the problem with this example:

// index.ts
import { AbstractParent } from "./AbstractParent";

const child = AbstractParent.createChild("circular dependency");
console.log(child.getText);

// AbstractParent.ts
import { Child } from './Child'

export class AbstractParent {
    private test: string;

    constructor(text: string) {
        this.text = text;
    }

    getText() {
        return this.text;
    }

    static createChild(text) : Child {
    const child = new Child('circular dependecies');
        return child;
    }
}

// Child.ts
import { AbstractParent } from './AbstractParent'

export class Child extends AbstractParent {
    constructor(text: string) {
        super(text);
    }
}

When running this code,

  • index.ts will import AbstractParent.ts at line 1
  • AbstractParent.ts will import Child.ts at line 1
  • Child.ts will import AbstractParent.ts at line 1, which it will receive from object cash. Then it will load the rest of the file. Note that at this point the AbstractParent.ts is empty, since it only reached line 1!!!
  • AbstractParent.ts will load the rest of the file.
  • When executing AbstractParent.createChild() it will try to reference an undefined AbstractParent constructor since it wasn’t fully defined at the moment of loading of Child.ts;

How to find them?

There are several tools that allow finding circular dependencies. The one that I’ve been using is named Madge. Madge is a feature rich tool for visualizing your dependencies that can also find circular dependencies.

Installing Madge

npm -g install madge

If you don’t want to install the tool globally, install it as a local development dependency. You will need to use npx to run madge.

npm -i -d madge

Using Madge

For JavaScript

# Madge installed globally
madge . -c

# Madge installed as dev dependency
npx magde . -c

For TypeScript

madge --extensions ts . -c

Result for the above example

Processed 2 files (0.3s) (1 warnings)
1) AbstractParent.ts > Child.ts

How to avoid / fix circular dependencies?

There are several ways we can use to reduce such dependencies.

Try keeping your codebase small

Split large application into modules and services.

Reduce number of dependencies

Split large files with multiple imports into smaller ones with a smaller number of imports.

Extract commonly used constants, functions or exports into separate files that other code references to

Example extracting exports. Consider the below code to the one in the previous examples. Here we moved the export directive to separate file. Not only it allows solving the circular dependencies, it also allows us to set the exact order of exporting the modules.

// internalExports.ts
export * from './AbstractParent'
export * from './Child'

// index.ts
import { AbstractParent } from "./internalExports";

const child = AbstractParent.createChild("circular dependency");
console.log(child.getText);

// AbstractParent.ts
import { Child } from './internalExports'

export class AbstractParent {
    private test: string;

    constructor(text: string) {
        this.text = text;
    }

    getText() {
        return this.text;
    }

    static createChild(text) : Child {
        const child = new Child('circular dependecies');
        return child;
    }
}

// Child.ts
import { AbstractParent } from './internalExports'

export class Child extends AbstractParent {
    constructor(text: string) {
        super(text);
    }
}

Extracting constants: Consider a group of classes where each class has a constant, or some other static component. Other classes use this constant for their internal logic, but have to import the entire file to use it.

// Dog.ts
import { lotsOfStuff } from './somewhere';

export class Dog {
    static final id = 'abcd';
    someMethod() {
        ...
    }
}

// Cat.ts
import { lotsOfStuff } from './somewhere';

export class Cat {
    static final id = 'efgh';
    someMethod() {
        ...
    }
}

// Client.ts
import { Cat } from './Cat';
import { Dog } from './Dog';
// The above lines imported many unrequired dependencies.

function someLogic(animalPassport: AnimalPassport) {
    if (animalPassport.id == Dog.id) {
        // Do something
    }
    if (animalPassport.id == Cat.id) {
        // Do something else
    }
}

Alternatively, we can extract the constants to another file, having all the clients referring to it. The new file will have no imports, or very few of them, reducing the possibility for circular dependencies.

// AnimalIds.ts
export dogId = 'abcd';
export catId = 'efgh';

// Dog.ts
import { dogId } from './AnimalIds';
import { lotsOfStuff } from './somewhere';

export class Dog {
    private static final id = dogId;
    someMethod() {
        ...
    }
}

// Cat.ts
import { catId } from './AnimalIds';
import { lotsOfStuff } from './somewhere';

export class Cat {
    private static final id = catId;
    someMethod() {
        ...
    }
}

// Client.ts
import { catId, dogId } from './AnimalIds';
// No unintended imports here!

function someLogic(animalPassport: AnimalPassport) {
    if (animalPassport.id == dogId) {
        // Do something
    }
    if (animalPassport.id == catId) {
        // Do something else
    }
}