Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Method override error with generic this- only for non-overloaded methods. #23960

Closed
zamb3zi opened this issue May 8, 2018 · 8 comments
Closed
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@zamb3zi
Copy link

zamb3zi commented May 8, 2018

TypeScript Version: 2.8.3

Search Terms: generic this, override, polymorphic this

Code

namespace N1 {
    /*
        The following method override generates:
        Property 'f' in type 'D' is not assignable to the same property in base type 'C'.
        Type '<T extends D>(this: T, arg: keyof T) => void' is not assignable to type '<T extends C>(this: T, arg: keyof T) => void'.
            The 'this' types of each signature are incompatible.
            Type 'T' is not assignable to type 'D'.
                Type 'C' is not assignable to type 'D'.
                Property 'y' is missing in type 'C'.
    
        Why is generic T treated differently to polymorphic this?
    */
    class C {
        x: number;
        f<T extends C>(this: T, arg: keyof T) { }
    }

    class D extends C {
        y: string;
        f<T extends D>(this: T, arg: keyof T) { }
    }
}

namespace N2 {
    /*
        By adding a pointless overload the method override works fine.
    */
    class C {
        x: number;
        f<T extends C>(this: T, arg: keyof T): void;
        f<T extends C>(this: T, arg: keyof T): void;
        f<T extends C>(this: T, arg: keyof T) { }
    }

    class D extends C {
        y: string;
        f<T extends D>(this: T, arg: keyof T) { }
    }
}

namespace N3 {
    /*
        The problem occurs similarly with static methods where polymorphic this is
        not available. This time the error occurs on the definition of D however.
        As before, overloading works around the issue.
    */
    type Constructor<T> = { new(...args: any[]): T };
    class C {
        x: number;
        static c: boolean;
        // static f<T extends C>(this: Constructor<T>, arg: keyof T): void;
        // static f<T extends C>(this: Constructor<T>, arg: keyof T): void;
        static f<T extends C>(this: Constructor<T>, arg: keyof T) { }
    }

    class D extends C {
        y: string;
        static d: boolean;
        static f<T extends D>(this: Constructor<T>, arg: keyof T) { }
    }
}

namespace N4 {
    /*
        This is another workaround using T extends C in the sub-class but still contraining this
        to be a D class or subclass.
    */
    type Constructor<T> = { new(...args: any[]): T };
    class C {
        x: number;
        static c: boolean;
        static f<T extends C>(this: typeof C & Constructor<T>, arg: keyof T) { }
    }

    class D extends C {
        y: string;
        static d: boolean;
        static f<T extends C>(this: typeof D & Constructor<T & D>, arg: keyof T) { }
    }
}

Expected behavior:

Non-static case: behavior should be the same, with and without use of overloading. It seems as if the overloaded behavior is correct. T should be treated as if it were polymorphic this.

Static case: this is more of a problem because this pattern seems to be the current best practice for achieving static polymorphic this (see: [#5863]). Both of the workarounds ought not be needed.

@ghost
Copy link

ghost commented May 8, 2018

If that were allowed there could be an error at runtime like so:

class C {
    x: number;
    f<T extends C>(this: T, arg: keyof T) { }
}

class D extends C {
    y: string;
    f<T extends D>(this: T, arg: keyof T) {
        // Allowed because `T` extends `D`.
        this.y.toUpperCase();
    }
}

function f(c: C) {
    // We think this is OK to do because `C#f` can take a `this` of any `T` extending `C`, including `C` itself.
    // Though note that TypeScript doesn't actually have a type-safe way to provide a `this` argument -- `call` just takes `...args: any[]`.
    c.f.call(new C(), "x");
}

f(new D()); // Allowed because `D` extends `C`.

Overloading breaks it because overloading is unsafe in general.

@zamb3zi
Copy link
Author

zamb3zi commented May 9, 2018

Thanks but your example does not produce a runtime error. It's clear that use of call, apply and bind is not type safe, so there are any number of ways to produce a runtime error that way.
I don't really see how overloading is unsafe in general, especially not in this case where the types are identical in the overload list and implementation.

@ghost
Copy link

ghost commented May 9, 2018

I tested the example and it shows a runtime error for me?

It's clear that use of call, apply and bind is not type safe, so there are any number of ways to produce a runtime error that way.

True, but that's only necessary because of the this parameter. The idea is the same for a regular parameter:

class C {
    x: number;
    f<T extends C>(self: T) { }
}

class D extends C {
    y: string;
    f<T extends D>(self: T) {
        self.y.toUpperCase();
    }
}

function f(c: C) {
    c.f(new C());
}

f(new D());

@zamb3zi
Copy link
Author

zamb3zi commented May 9, 2018

Exactly. I think the problem is that this: T is being treated as if it were a normal parameter. In that case it's absolutely a problem that the T in C.f "is not assignable to type D". However, polymorphic this is different since it's defined as a sub-type of the class in which the method is defined. That's why the following is OK, even though it's equivalent to the original N1 example:

class C {
    x: number;
    f(arg: keyof this) { }
}

class D extends C {
    y: string;
    f(arg: keyof this) { }
}

@ghost
Copy link

ghost commented May 9, 2018

Why would you write f<T extends C>(this: T) if you didn't want to accept any arbitrary T as the this parameter?
this is not always guaranteed to be an instance of the enclosing class if you specified otherwise. You can even write f(this: number) { this.toFixed(); } and TypeScript will accept it, and correctly not allow you to call new C().f(); in that case.

@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@saschanaz
Copy link
Contributor

saschanaz commented Aug 23, 2018

Why is this closed as Working as Intended? I think the point of this issue is the inconsistency:

By adding a pointless overload the method override works fine.

IMO f<T extends C>(this: T, arg: keyof T) { } and f<T extends C>(this: T, arg: keyof T): void; f<T extends C>(this: T, arg: keyof T): void; f<T extends C>(this: T, arg: keyof T) { } should be same, no? This is still can be reproduced in the playground.

@saschanaz
Copy link
Contributor

Filed #26631 with a minimal repro.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants