I'm using TypeScript with --module system
(SystemJS) in a very large project. SystemJS supports cyclic dependencies, and most of the time it works fine. However, when TypeScript inheritance gets involved, things begin to break.
For example, if a class A
depends on class B
, and class B
inherits from class A
, then if class A
gets loaded first:
- It will pause
class A's
resolution and will try to load the class B
dependency
class B
will think its dependencies are resolved, since class A
has been touched.
class B's
inheritance will fail to resolve, because class A
is still undefined.
Most of the "solutions" I can find on the web to circular dependencies with module loaders are either:
- Change your design / combine classes into a single module
- CommonJS and non-TypeScript specific workarounds
I feel like there are valid justifications for circular designs, and combining classes into giant files is not always desirable, so please consider these workarounds to be off topic for the question that I am asking.
Are there any solutions to the actual problem?
Changing your design is the most favourable solution. A class should not depend on its subclasses. If you use them in a factory or so, that is a separate concern and should go in a separate class/function/module.
Are there any solutions to the actual problem?
As you said, the problem occurs only when module A is loaded first. The solution is to prevent that, and write an extra module that acts as a proxy to A and all its subclasses while importing them in the correct order.
In this case I suggest you remove the dependency of A -> B
by creating a separate interface I
. Both A
and B
need to know I
, and B
needs to implement it.
During your load process B
must tell A
where to find a constructor or factory for I
(implemented by B
). This will leave you with those dependencies:
A -> I
B -> I
B -> A
Interface I
could look like this:
interface I {
bFoo(): void;
}
export default I;
Class A
could look like this:
import I from "./i";
class A {
private static __ICtor : new() => I;
public static setIConstructor(ctor: new() => I) {
A.__ICtor = ctor;
}
private __atSomePoint() : I {
return new A.__ICtor();
}
}
export default A;
And finally class B
:
import I from "./i";
import A from "./a";
class B extends A implements I {
public bFoo() {}
}
A.setIConstructor(B);
IMHO this would solve your cyclic dependency, even if it is too late by now.
There's a neat way to solve this issue by using the Babel transform plugin here: https://github.com/zertosh/babel-plugin-transform-inline-imports-commonjs
What it does is it converts the at-start-of-file module imports into inline requires that only actually import/require the other modules just before they're used.
In most cases, this solves the problem automatically, as by the time any class-using code actually runs, the modules have all completed their exports.
Note that by default the plugin above applies to all imports in your project, turning them all into inline requires. A more serious issue, however, is that I couldn't find a built-in way to make it work with relative-path imports/requires.
I fixed both these issues in my fork of the project here: https://github.com/Venryx/babel-plugin-transform-inline-imports-commonjs
I made a pull request for these changes, but it's awaiting review atm.