Michael Tiller
Michael Tiller

Reputation: 9421

Polymorphic this in TypeScript

I'm trying to do what I think is a "textbook" use case for polymorphic this and it isn't working and I just can't figure this out. Imagine I have some abstract base class that is cloneable, e.g.:

abstract class X {
    abstract clone(): this;
}

Now I want to implement a base class that provides a clone implementation:

class Y extends X {
    constructor(private x: number) { super() }
    clone(): this { return new Y(this.x + 1); }
}

When I do this, I get an error that says Type Y is not assignable to type 'this'. I'm totally confused. All I want to convey here is the type constraint that if a subclass of X has its clone method invoked, that the type of the thing that you will get back will be identical to the subtype. Isn't this exactly what polymorphic this is for? What am I doing wrong here?

Here is a link to this code in the TypeScript playground.

Upvotes: 6

Views: 1017

Answers (2)

Michael Tiller
Michael Tiller

Reputation: 9421

I've marked artem's answer as the correct one because he seems to be correct that it really isn't possible to do this in a 100% safe way.

However, there is a way to get the compiler to enforce the type constraint I wanted. So, I decided to include this answer in case it is useful to people. The only downside of my approach is that it is slightly unsafe for exactly the reasons that artem points out, i.e., that somebody could extend your classes without providing an implementation of clone and create a situation where the returned value would not really be what you claim it is.

My solution was to just add a cast. Again, this is unsafe in general. But if you never extend from the classes, it works fine as far as I can tell. So my solution was:

class Y extends X {
    constructor(private x: number) { super() }
    clone(): this {
        return new Y(this.x + 1) as this;
    }
}

You can see a complete version that compiles here.

Upvotes: 5

artem
artem

Reputation: 51629

No, as things are right now, it's not a supported use case for this type. A method with this return type should return an instance of derived class even if this method is not overridden in the derived class - see code in this answer for an example that justifies this.

In other words, given abstract clone(): this declaration, this code must be valid even if Z does not override clone itself, but you implementation of clone in Y breaks it.

class Z extends Y {
    f() {
        let c: Z = this.clone();
    }
}

So it looks like a method with this return type should always return this.

UPDATE: I found an open issue with this use case on github, marked as 'accepting pull requests'. I'm not sure however if they really intend to support it, or plan to fix cloneNode in their compiler in some other way.

Upvotes: 4

Related Questions