Home > front end >  Make constructor return subclass
Make constructor return subclass

Time:02-01

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 returns 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 newing 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") //            
  •  Tags:  
  • Related