Skip to content

"The 'this' context of type 'Foo | Bar' is not assignable to method's 'this' of type 'Foo'" with a this constraint that shouldn't have any effect #54407

@Jym77

Description

@Jym77

Bug Report

🔎 Search Terms

The this context of type is not assignable to methods this of type

Found #23304, #28777, and #51198 which all seem somewhat different.

🕗 Version & Regression Information

Passing in TS 3.5.1, failing in TS  3.6.3 and above (including nightly 5.2.0-dev.20230526)

  • This changed between versions 3.5.1 and 3.6.3
    (haven't tried intermediate versions since they are not in the Playground)
    (not seeing anything obvious in the 3.6 release notes

⏯ Playground Link

Playground link with relevant code

💻 Code

class Foo {
  foo = 1;

  public ok(): void {}

  public ko(this: Foo): void {}

  public overload_ok(): void;
  public overload_ok(this: Foo): void;
  public overload_ok(): void {}

  public overload_ko(this: Foo): void;
  public overload_ko(): void;
  public overload_ko(): void {}
}

class Bar {
  public ok(): void {}

  public ko(): void {}

  public overload_ko(): void {}

  public overload_ok(): void {}
}

function f(x: Foo | Bar): void {
  x.ok();
  x.ko();                     // error
  // The 'this' context of type 'Foo | Bar' is not assignable to method's 'this' of type 'Foo'. 
  // Property 'foo' is missing in type 'Bar' but required in type 'Foo'.

  x.overload_ok();
  x.overload_ko();     // error
  // The 'this' context of type 'Foo | Bar' is not assignable to method's 'this' of type 'Foo'.
  // Type 'Bar' is not assignable to type 'Foo'.
}

function g(x: Foo | Bar): void {
  if (x instanceof Foo) {
    x.ok();
    x.ko();
    x.overload_ok();
    x.overload_ko();
  } else {
    x.ok();
    x.ko();
    x.overload_ok();
    x.overload_ko();
  }
}

🙁 Actual behavior

The calls to x.ko() and x.overload_ko() throw type errors in f.

🙂 Expected behavior

So, the signatures of ko and ok are slightly different (in Foo), but since ko is already a method on the class Foo, it should only be called when this is an instance of Foo anyway, so it feels like the "this constraint" shouldn't do anything.

The overload_* cases feel even weirder. In overload_ok, the first overload is definitely a catch all, so it is the only one used (as far as I understand), and since it is the same as the ok signature, everything works.
But in overload_ko, the first overload is the same as the ko signature and fails for the same reason, but the second overload (which is the same as the ok signature and thus should work) is not even tried (my IDE also says it is never called). So, it seems that TS treats the first overload (of overload_ko) as a catch all case and stops looking further… except it is actually not behaving the same way. TS even tries to be helpful adding:

public overload_ko(): void {}
       ~~~~~~~~~~~
The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.

Since the implementation signature is exactly the same as the second overload, this shows that TS totally ignored the second overload, yet doesn't treat the first one as being fully a catch all case, since stuff breaks with the first one that would work with the second.


g is a workaround where forcing the narrowing and breaking the union in advance resolves the problem. However, having to WET my code is not really doable in practice; and having both the "then" and "else" branchs with the exact same code is such an anti-pattern that it makes my programmer lizard brain scream 🙈


Of course, there may be something I do not really understand in what the "this constraint" actually does. This looks very weird from where I stand now.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions