Updated on January 3, 2023
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 importAbstractParent.ts
at line 1AbstractParent.ts
will importChild.ts
at line 1Child.ts
will importAbstractParent.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 theAbstractParent.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 undefinedAbstractParent
constructor since it wasn’t fully defined at the moment of loading ofChild.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 eliminate, or at least reduce such dependencies.
Modularize your code
Having circular dependencies in your code, is pretty much a code smell that indicates an incorrect organization of your code base. Writing well organized and modularized code, leads to circular dependencies to be a non-issue. Consider splitting into smaller modules, using dependency injection and perhaps frameworks like Nest.js to achieve higher levels of code modularization.
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
}
}