tslint rules to work around circular dependencies
This package contains four rules, including fixes, to work around some of the issues
that arise with circular imports:
imports-after-export
initialize-statics-after-imports
new-instance-after-imports
no-instanceof-operator
Usage
Install
npm install -D tslint-circular-dependencies
This will install the rules and set up your tslint.json file.
TypeScript 2.4.1
These rules have been tested with TypeScript 2.4.1. If you're seeing no output when you run these rules, try updating TypeScript to this version.
Run
tslint [path] --fix
Manually configuring tslint.json (optional)
This package will install itself into your tslint.json as follows:
"extends" : [
...
"tslint-circular-dependencies"
...
]
Keep the rule names intact. tslint does not document a certain execution order for rules, but right now they are executed in alphabetic order. It is important that the rules in this package are executed in a particular order, and thats 1. imports-after-export, 2. initialize-statics-after-imports, 3. new-instance-after-imports.
Inside the Rules
imports-after-export
Moves all import statements – except those that are used as superclasses in an extends clause – after the last export statement.
Why
Circular imports work if the import occurs after export.
The following code will fail:
// a.ts
import { B } from './b';
export class A {
constructor() {
this.b = new B();
}
}
// b.ts
import { A } from './a';
export class B {
constructor() {
this.a = new A();
}
}
This fails because at the time the import is executed, module.exports is still undefined.
| Step |
Statement |
a.exports |
b.exports |
| 1 |
import { B } from './b'; |
undefined |
undefined |
| 2 |
import { A } from './a'; |
undefined |
undefined |
| 3 |
export class B {...} |
undefined |
class B |
| 4 |
export class A {...} |
class A |
class B |
The following code will work:
// a.ts
export class A {
constructor() {
this.b = new B();
}
}
import { B } from './b';
// b.ts
export class B {
constructor() {
this.a = new A();
}
}
import { A } from './a';
| Step |
statement |
a.exports |
b.exports |
| 1 |
export class A {...} |
class A |
undefined |
| 2 |
import { B } from './b'; |
class A |
undefined |
| 3 |
export class B {...} |
class A |
class B |
| 4 |
import { A } from './a'; |
class A |
class B |
initialize-statics-after-imports
This rule
- encapsulates non-primitive
static initializers in a static function
- executes that
static function after all import statements
Improvement Suggestion
- Only move initializers that reference imported symbols. This rule currently encapsulates all
static initializers into a function. This could be more accurately move only those static initializers that reference imported symbols. Keep in mind this may mean you'd have to analyze the execution path in case of initializers like this:
class A {
public static x = A.initializeX();
private static initializeX() {
return new X(); // this will fail because X isn't imported yet
}
}
import { X } from 'X';
new-instance-after-imports
This rule moves new expressions to after the last import statement to make sure all dependencies have been loaded.
class A {
...
}
new A(); // will be moved to after `import` statement
import { B } from 'B';
no-instanceof-operator
This rule changes any use of the instanceof operator with a constructor.name comparison, e.g.
Original code before this rule:
class A {}
let a = new A();
a instanceof A;
Original code after this rule:
class A {}
let a = new A();
a.constructor.name === 'A';
tslint-no-circular-imports - TSLint plugin to detect and warn about circular imports
dependency-cruiser – Validate and visualize dependencies. With your rules. JavaScript. TypeScript. CoffeeScript. ES6, CommonJS, AMD.