About why I do not use generic: the entry like new B<B>(data) or new C<C>(data) looks strange to me.
You need to use generics in order to do this, but B and C don't need to be generic. They can extend specific types of the generic base A. So you'll just call new B(data).
We can derive some of the required types just from looking at the usage.
this.nodes = data.children.map(child => new instance(child));
This line here tells me that the data variable which is passed to the constructor has a children property. children is an array where every element is assignable to the same type as data.
children seems to be required here, in which case terminal nodes would have {children: []}. That's fine if that's what you want, but typically it is optional such that terminal nodes do not have a children property at all.
type Recursive<OwnData> = OwnData & {
children?: Array<Recursive<OwnData>>;
}
So we get this type Recursive<OwnData>, which is a generic that depends on a variable OwnData where OwnData represents all properties other than children.
We do not know what goes on inside the constructors of individual descendants of A and we do not know what unique instance variables they might declare. So for maximum flexibility, we should assume that the properties of data and the properties of a node aren't always the same. This means that A needs two generics.
class A<DataProps, NodeType> {
public nodes: Array<NodeType>;
constructor(
data: Recursive<DataProps>,
instance: new (data: Recursive<DataProps>) => NodeType
) {
this.nodes = data.children ? data.children.map(child => new instance(child)) : [];
}
}
Now when we create a subclass, we extend a specific version of A<DataProps, NodeType>. We can pass the subclass itself as the type for NodeType.
type BData = {
b: number;
}
class B extends A<BData, B> {
public b: number;
constructor(data: Recursive<BData>) {
super(data, B);
this.b = data.b;
}
someFunc(): void {
console.log(this.b);
}
}
const bInst = new B({b: 1, children: [{b: 2}, {b: 3}]});
// each node of `bInst` is `B`
bInst.nodes.map(b => b.someFunc() );
Typescript Playground Link
dataandinstancevariables and it would be good.instance, because it can be any inherited class from class A (same fordata, cause it depends from the class)new B(data)that calls the super constructor and the super constructor invokesnew Bagain (asnew instance). Which should logically call the super constructor and go into an infinite recursion. Or maybe just throw an error. I suppose it depends on what shapedatahas. And I'm still not sure why you're not using generics here. This doesn't feel like an is-a relationship betweenA,B, andC. You might need composition to solve your design problem first.new instanceis called only for child nodes (thus, it is called as long as each subsequent node has child nodes). about why I do not use generic: the entry likenew B <B> (data)ornew C<C>(data)looks strange to me. But at the same time I would still like to know which of the solutions is the best...