Skip to content

Class method with conditional return type cannot return types common to both conditional branches when the class also has an interface #50822

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

Open
ascott18 opened this issue Sep 18, 2022 · 6 comments
Labels
Bug A bug in TypeScript
Milestone

Comments

@ascott18
Copy link

Bug Report

πŸ”Ž Search Terms

method conditional return type not assignable to type

πŸ•— Version & Regression Information

3.3.3+

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

export interface Foo<T> {}
export class Foo<T> {
  public bar(): T extends string ? string | number : number {
    return 1;
  }
}

πŸ™ Actual behavior

Return statement is an error, despite being valid for both branches of the conditional return type.

This ONLY happens if the interface Foo exists. If the interface is commented out, the error goes away.

πŸ™‚ Expected behavior

Return statement is not an error, because 1 is assignable to both string | number and number.

@RyanCavanaugh
Copy link
Member

Duplicate #27932

You can write

  public bar(): number | (T extends string ? string : never) {

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Sep 20, 2022
@ascott18
Copy link
Author

@RyanCavanaugh Thanks for looking into it. I'd be really curious to know why the mere presence of the empty interface triggers the issue, and why the issue does not occur if I comment out the interface. I see the linked issue is an abandoned PR - does that mean this is a wontfix?

@RyanCavanaugh RyanCavanaugh removed the Duplicate An existing issue was already created label Sep 21, 2022
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Sep 21, 2022

I missed that - no, that's super weird

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Sep 21, 2022
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Sep 21, 2022
@Andarist
Copy link
Contributor

Sideways question - what does this construct even mean? Based on my testing this seems like a huge footgun.

export interface Foo<T> {
  foo(): number
}
export class Foo<T> {
  constructor(private _v: T) {}
  public bar(): T {
    return this._v
  }
}

const inst = new Foo(100)

inst.foo() // ok, but crashes at runtime
inst.bar() // ok

@RyanCavanaugh
Copy link
Member

Declaration merging, for declaring when you've e.g. polyfilled foo onto the Foo.prototype

@Andarist
Copy link
Contributor

This happens because a literal 1 type (source) is compared against a conditional type (target). When a conditional type is on the target side the types are only related to each other when the conditional type:

  • has no infer type parameters
  • and is distribution independent

This logic can be found here. In turn, isDistributionDependent checks if the root.checkType type parameter is possibly referenced and, for this case, it will always return true and classify this conditional as dependent on the distribution and thus the source won't be checked against the target at all. The reason why this type param is treated as possibly referenced is that the symbol of this type param has more than one declaration, you can check the logic here

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

No branches or pull requests

3 participants