When a class constructor
method explicitly uses a return
statement to return an object that is not this
, the object returned will be received when the class constructor is called with the new
operator, instead of the new object created by the constructor.
class Foo {
fooProp: string;
constructor() {
this.fooProp = "abc";
return {
fooProp: "zyx",
}
};
}
const f = new Foo();
console.log(f.fooProp) // Output: zyx
This scenario is not common and can lead to unexpected outcomes, such as instances not being recognized as part of their own constructor:
console.log(new Foo() instanceof Foo) // Output: false
It is advised to avoid utilizing this pattern unless necessary for specific reasons.
In TypeScript, it is possible to use return
in a constructor
method if the returned object belongs to a type compatible with the class type. This approach is acceptable when returning subclasses because they are directly related to the parent class.
One way to implement this behavior is demonstrated below:
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 {
f() { console.log("C") }
}
const a = new A().f(); // Output: A
const b = new A("B").f(); // Output: B
const c = new A("C").f(); // Output: C
By making A
's constructor parameter optional and creating B
and C
without constructor parameters, you can selectively return subclass instances based on input strings while maintaining hierarchy within the classes.
// Testing the implementation:
const a = new A();
a.f(); // Output: A
const b = new A("B");
b.f(); // Output: B
const c = new A("C");
c.f(); // Output: C
console.log(a instanceof A); // true
console.log(b instanceof A); // true
console.log(c instanceof A); // true
This setup ensures expected results with clear distinctions between direct instances of A
and its subclasses.
Although effective, using A
's constructor for dual purposes can lead to confusion and errors. Consider making A
's constructor
a protected
method and employing a static
method for selecting the appropriate constructor to call, as illustrated below:
class A {
protected constructor() { }
f() { console.log("A") }
public static make(input?: string) {
if (input === "B") return new B();
if (input === "C") return new C();
return new A();
}
}
class B extends A {
f() { console.log("B") }
}
class C extends A {
f() { console.log("C") }
}
const a = A.make();
a.f(); // Output: A
const b = A.make("B");
b.f(); // Output: B
const c = A.make("C");
c.f(); // Output: C
console.log(a instanceof A); // true
console.log(b instanceof A); // true
console.log(c instanceof A); // true
Despite slight differences in usage syntax, this method provides a clearer understanding of class instantiation choices for both internal implementations and external calls.
Explore the code in TypeScript Playground