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

In some circumstances a derived class is not assignable to its base class under --strictFunctionTypes #27047

Closed
JakeTunaley opened this issue Sep 12, 2018 · 3 comments
Assignees
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@JakeTunaley
Copy link

JakeTunaley commented Sep 12, 2018

TypeScript Version: 3.1.0-dev.20180912

Search Terms: class extends assignability strict function types this

Code

class Base {
    private foo (): (this: this) => void {
        return (() => {}).bind(this);
    }
}

class Derived extends Base {
    private bar () {}
}

const value: Base = new Derived();

Expected behavior:
No errors.

Actual behavior:
The following error is generated on the constant value:

Type 'Derived' is not assignable to type 'Base'.
  Types of property 'foo' are incompatible.
    Type '() => (this: Derived) => void' is not assignable to type '() => (this: Base) => void'.
      Type '(this: Derived) => void' is not assignable to type '(this: Base) => void'.
        The 'this' types of each signature are incompatible.
          Type 'Base' is not assignable to type 'Derived'.
            Property 'bar' is missing in type 'Base'.

This only happens when --strictFunctionTypes is enabled.

This shouldn't happen because TS will not let you change a class' type signature in a way that breaks the extends clause - Derived is always a superset of Base.

Playground Link:
http://www.typescriptlang.org/play/#src=class%20Base%20%7B%0D%0A%09private%20foo%20()%3A%20(this%3A%20this)%20%3D%3E%20void%20%7B%0D%0A%09%09return%20(()%20%3D%3E%20%7B%7D).bind(this)%3B%0D%0A%09%7D%0D%0A%7D%0D%0A%0D%0Aclass%20Derived%20extends%20Base%20%7B%0D%0A%09private%20bar%20()%20%7B%7D%0D%0A%7D%0D%0A%0D%0Aconst%20value%3A%20typeof%20Base%20%3D%20Derived%3B

@ghost ghost added the Bug A bug in TypeScript label Sep 12, 2018
@RyanCavanaugh RyanCavanaugh added Needs Investigation This issue needs a team member to investigate its status. and removed Bug A bug in TypeScript labels Oct 9, 2018
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.3 milestone Oct 9, 2018
@RyanCavanaugh
Copy link
Member

I think this is working as intended because the this in the return type is unsound to aliasing. Let's say we let you do this:

class Base {
	foo (): (arg: this) => void {
		return function () {
		}
	}
}

class Derived extends Base {
	foo (): (arg: this) => void {
		return function (arg) {
			arg.bar();
		}
	}

	bar() { }
}

const b = new Base();
const value: typeof Base = Derived;
// Typechecks OK, but crashes 
(new value()).foo()(b);

@JakeTunaley
Copy link
Author

JakeTunaley commented Oct 15, 2018

Okay, I think I understand. I'm expecting this to be an object instance unit type, when in reality it's just a special case of a type alias that takes the value of whatever its containing type is. That would also explain #26759.

I guess I understand why it's like that, because keeping track of object instances through code like the following would likely tank compiler perf:

// Assuming "ExactInstance" is some imaginary type that acts as an object instance unit type

class Foo {
    bar (arg: ExactInstance<this>) {}
}

const foo = new Foo();

function callBar (arg: ExactInstance<typeof foo>) {
    foo.bar(arg);
}

callBar(foo); // Fine
callBar(new Foo()); // Error
const foo2 = foo;
callBar(foo2); // Fine

Although if this behaves like a type alias, then the following is inconsistent:

class Foo {
    a (): this {
        // Error: Type 'Foo' is not assignable to type 'this'.
        return new Foo();
    }

    b (arg: this): this {
        return arg;
    }
}

const value = new Foo();
value.b(new Foo()); // But this call is completely fine...?

@sandersn
Copy link
Member

The hidden type parameter Base<This> becomes invariant because of foo. This is inconsistent, but making the extends check stricter would break tons of code, so I think it has to stay this way.

The last example works because, for value, This=Foo when you say new Foo(). So the instantiated type of b: (arg: Foo) => Foo.

@sandersn sandersn added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Mar 13, 2019
@RyanCavanaugh RyanCavanaugh removed the Needs Investigation This issue needs a team member to investigate its status. label Mar 14, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

3 participants