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

type guard on this inside method produces strange narrowings and seemingly inconsistent error messages #47903

Open
jcalz opened this issue Feb 15, 2022 · 1 comment
Labels
Bug
Milestone

Comments

@jcalz
Copy link
Contributor

@jcalz jcalz commented Feb 15, 2022

Bug Report

πŸ”Ž Search Terms

this type guard, method, hidden circularity, annotated return type, user defined type guard

πŸ•— Version & Regression Information

  • This changed between versions 3.7.5 and 3.8.3

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

class Foo<T extends string | number> {
  constructor(public prop: T) { }

  method() {
    if (hasStringProp(this)) {
      return this.prop.toUpperCase() // error! 
      // Property 'toUpperCase' does not exist on type 'string | number'
    }
    if (hasNumberProp(this)) {
      return this.prop.toFixed(); // error! 
      // Property 'prop' does not exist on type 'never'
    }
    throw new Error();
  }
}

function hasStringProp(t: Foo<string | number>): t is Foo<string> { return typeof t.prop === "string" }
function hasNumberProp(t: Foo<string | number>): t is Foo<number> { return typeof t.prop === "number" }

πŸ™ Actual behavior

Inside the respective blocks in method(), this is seen (via quickinfo) to have been narrowed to this & Foo<string> and then this & Foo<number>, and this.prop is seen to have been narrowed to T & string and then T & number. But actually trying to access this.prop as such produces errors about how this.prop is apparently string | number (as if no narrowing has occurred) in the first case, and how this is apparently never (as if too much narrowing has occurred) in the second case.

It really seems like the actual problem here is a hidden circularity. The return type of method() depends on the return type of hasStringProp(this) and hasNumberProp(this) which depends on Foo which depends on the method(). But no circularity warning is issued. Instead there's just these weird narrowing errors.

If you annotate the return type of method() explicitly, this problem goes away (starting with TS version 4.1.0-dev.20201028):

  method(): string {
    if (hasStringProp(this)) {
      return this.prop.toUpperCase(); // okay
    }
    if (hasNumberProp(this)) {
      return this.prop.toFixed(); // okay
    }
    throw new Error();
  }

πŸ™‚ Expected behavior

I'd expect either everything should just work, or if there's a circularity issue then it should be mentioned explicitly on method().

Comes from this Stack Overflow question

@RyanCavanaugh RyanCavanaugh added the Bug label Feb 23, 2022
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Feb 23, 2022
@aspic-fish
Copy link

@aspic-fish aspic-fish commented Mar 27, 2022

another workaround would be

const self = this as Expand<this>;

where Expand is

type Expand<T> = 
  T extends infer R 
    ? {[key in keyof R]: R[key]}
    : never;

Playground link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug
Projects
None yet
Development

No branches or pull requests

3 participants