Hello, the following code example throws the error:
TypeError: Super constructor null of SecondChild is not a constructor
at new SecondChild (<anonymous>:8:19)
at <anonymous>:49:13
at dn (<anonymous>:16:5449)
Currently i am trying to understand what exactly the issue is about and how to implement the factory pattern in typescript/javascript. There are several behaviours which i don´t quite understand:
It would be possible to prevent the error if i implemented the parent class without using the type "this" in combination with the factory Method but it seems pretty convenient. The goal behind the abstracted method duplicate is that it returns a subclass instance. I could implement it in every subclass but in my real world project the complexity is much higher.
src/
child.ts
factory.ts
index.ts
parent.ts
main.ts
second-child.ts
// Throws error
import { Child, Parent, SecondChild } from './index'
console.log(new Child().duplicate())
console.log(new SecondChild().duplicate())
console.log(new Parent().duplicate())
// Works as intended:
// import { Child, Parent} from './index'
// console.log(new Child().duplicate())
// console.log(new Parent().duplicate())
import { factory } from './factory'
export class Parent {
isChild = 0;
duplicate(): this {
return factory(this.isChild) as unknown as this;
}
}
import {Parent} from './parent'
export class Child extends Parent{
override isChild = 1;
}
import {Parent} from './parent'
export class SecondChild extends Parent{
override isChild = 2;
}
import { Child } from './child'
import { SecondChild } from './second-child'
import { Parent } from './parent'
export function factory(child: number):Child;
export function factory(...args: unknown[]):Parent {
switch (args[0]) {
case 1: {
return new Child()
}
case 2: {
return new SecondChild()
}
default: {
return new Parent();
}
}
}
export * from './child'
export * from './second-child'
export * from './parent'
export * from './factory'
Circular dependencies between modules are a complex beast. The order of evaluation depends on the order of the import
statements and the entry point, which is very fragile. JS runs the code in a module after its dependencies are met, doing a DFS graph traversal, but it has to ignore dependencies that were already visited and are still waiting for evaluation of their dependencies.
In your case, that means
Parent
variable is already set up but still not initialised, throwing an exception.In your case, you can fix this by changing the imports in factory.ts to
import { Child, SecondChild, Parent } from './index';
Now, the traversal looks as follows:
factory
function, which - since it is not immediately called - does not attempt to access the uninitialised imported variablesParent
.Child
(given Parent
is already initialised)SecondChild
factory()
function, whose imports are now initialisedA tool like dpdm
will help you understand this even for large dependency graphs. And it'll urge you to avoid the circular dependencies, they're more trouble than they're worth, if you can easily avoid them.
In your case I would recommend to implement duplicate
without factory
but rather by conventional cloning (see here or there), or to implement factory
by using a class registry that the classes can register themselves with instead of importing them all into factory.ts.