Reputation: 14668
In the pull request for adding polymorphic this, one use case given is a clone method. However, when I attempted an implementation of this, I hit the same problem as shelby3 noted in the comment chain:
how can polymorphic clone(): this be implemented if it is not legal to assign an instance of the subclass to this??? It seems instead the compiler will need to deduce that inheritance from Entity types clone() as an abstract method even though the base has an implementation, so as to insure that each subclass provides a unique implementation.".
Is there a solution to this, or do I just have to use casts to force typescript to compile such code, and hope that any sub classes don't forget to implement clone themselves?
See this example, without the <this>
casts, the code would fail to compile. It also fails to detect that c.clone()
has the wrong type:
interface Cloneable {
clone() : this
}
class ClassA implements Cloneable {
a: number = 0;
clone() : this {
let result = new ClassA();
result.a = this.a;
return <this>result;
}
}
class ClassB extends ClassA {
b: number = 0;
clone() : this {
let result = new ClassB();
result.a = this.a;
result.b = this.b;
return <this>result;
}
}
class ClassC extends ClassB {
c: number = 0;
// missing clone method
}
function test() {
const a = new ClassA();
const b = new ClassB();
const c = new ClassC();
const aClone = a.clone(); // aClone : ClassA
const bClone = b.clone(); // bClone : ClassB
const cClone = c.clone(); // typescript thinks cClone is ClassC, but is actually ClassB
alert(cClone.constructor.name); // outputs ClassB
}
test();
Upvotes: 1
Views: 461
Reputation: 327624
I agree with you that there's no obvious type-safe way to override clone()
or any method that returns something that depends on polymorphic this
.
Polymorphic this
is like a generic type parameter that only becomes specified when you have a concrete instance of a class. Like a generic type parameter, it becomes difficult for the compiler to verify assignability to such a type when it is still unspecified, such as inside the implementation of such a function:
function similar<This extends { x: string }>(): This {
return { x: "" }; // error
}
The above is (rightly) an error because the compiler isn't sure whether or not {x: ""}
will actually be assignable to whatever This
is later specified as. If you want the above to compile without error, you need to use a type assertion.
This is basically what you have to do in the implementation of clone()
in each class, and using such a type assertion suppresses the error in exchange for the very real possibility that someone will come along later (with a subclass) and violate your asserted type.
So, what can be done? I haven't found anything I feel great about. Everything feels like a workaround. The hackiest workaround would be to get back some runtime safety by putting a check in the base class's constructor, like this:
class ClassA implements Cloneable {
a: number = 0;
// add this constructor
constructor() {
if (!this.constructor.prototype.hasOwnProperty("clone")) {
throw new Error("HEY YOU! " + this.constructor.name +
" is MISSING a clone() implementation! FIX THAT!!")
}
}
clone(): this {
let result = new ClassA();
result.a = this.a;
return <this>result;
}
}
Since you know every subclass instance will end up calling the base class constructor, you will get the following runtime error with your example code:
Error: HEY YOU! ClassC is MISSING a clone() implementation! FIX THAT!!
So, that at least gives any would-be subclass implementers a fighting chance of noticing what's going on, as long as there's some runtime testing. Compile time type safety would be nicer, of course, so 🤷♂️
The other approach would be to try to come up with a base class implementation of clone()
that behaves polymorphically and doesn't need to be subclassed. This is still kind of fragile, in that some subclass could do some weird stuff that the base class's clone()
implementation didn't anticipate, or the subclass could go ahead and override clone()
which we don't want (and we can't prevent that with something like final
... fun read about that) but it at least works for your example code:
class Clonable {
clone(): this {
const ret: this = Object.create(this.constructor.prototype);
Object.assign(ret, this);
return ret;
}
}
We make Clonable
a base class instead of an interface, since the implementation comes with it. Then your subclasses do nothing to clone()
:
class ClassA extends Clonable {
a: number = 0;
}
class ClassB extends ClassA {
b: number = 0;
}
class ClassC extends ClassB {
c: number = 0;
}
And your tests work as you want:
const c = new ClassC();
const cClone = c.clone(); // cClone : ClassC
alert(cClone.constructor.name); // outputs ClassC
That's about the best I can do. Hope it helps; good luck!
Upvotes: 2