class A{
constructor(input: string) {
// do stuff
}
f() {console.log("A")}
}
class B extends A{
constructor(input: string) {
// do stuff
}
f() {console.log("B")}
}
class C extends A{
constructor(input: string) {
// do stuff
}
f() {console.log("C")}
}
I currently have the following setup. This might sound weird, but I would like calling new A(inp)
to return me an instance of B or C according to the input (even if it's returned as A
according to TypeScript) so that calling (new A("B")).f()
will print "B", the same for passing "C", while the default is "A". Is this possible in a way that doesn't violate anything horribly? At the moment I am using a static method in A to achieve this, but I thought using the constructor would be neater.
CodePudding user response:
If a class constructor
method explicitly return
s an object other than this
, then when you use the new
operator on the class constructor, you will get that object instead of the new one created (which is called this
inside the body of the constructor method).
class Foo {
fooProp: string;
constructor() {
this.fooProp = "abc" // initialize fooProp on the new object
return {
fooProp: "zyx", // but return something completely different
}
};
}
const f = new Foo();
console.log(f.fooProp) // zyx, not abc
This is not very common, and has some surprising consequences, such as that the value you get from new
ing a constructor is not necessarily an instanceof
that constructor:
console.log(new Foo() instanceof Foo) // false !
People tend to avoid doing this unless they have a compelling reason. (It's up to you to decide if your reason is compelling enough).
TypeScript does let you return
in a constructor
method as long as the object returned is of a type assignable to the class type. In your case that's fine, because you are explicitly returning subclasses. So on the face of it, yes this is possible.
Here's one way to do it:
class A {
constructor(input?: string) {
if (input === "B") return new B();
if (input === "C") return new C();
}
f() { console.log("A") }
}
class B extends A {
constructor() {
super()
}
f() { console.log("B") }
}
class C extends A {
// still has implicit constructor that calls super
f() { console.log("C") }
}
const a = new A().f(); // A
const b = new A("B").f(); // B
const c = new A("C").f(); // C
I made A
's constructor parameter optional, and made B
and C
not take any constructor parameters. If you call the A
constructor with the exact strings "B"
or "C"
you get the subclass instances, otherwise you get a direct instance of A
. And since B
and C
instances are also instanceof A
, then you don't get the bizarre behavior that new A("B") instanceof A === false
as mentioned above. Let's test it:
const a = new A();
a.f(); // A
const b = new A("B");
b.f(); // B
const c = new A("C");
c.f(); // C
console.log(a instanceof A); // true
console.log(b instanceof A); // true
console.log(c instanceof A); // true
So that works exactly was you want.
But it's a bit concerning to me because A
's constructor has two quite different roles and if you mix them up then bad things can happen. The subclass constructors B
and C
must call the super()
method either explicitly (as in B
) or implicitly (as in C
), so you end up with the equivalent of new A()
called where you really only want a direct instance of A
. Meanwhile external callers sometimes want a B
or a C
. If you accidentally confuse these roles and do this:
class B extends A {
constructor() {
super("B") //