TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

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

Open ascott18 opened this issue 3 years ago • 3 comments

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.

ascott18 avatar Sep 18 '22 03:09 ascott18

Duplicate #27932

You can write

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

RyanCavanaugh avatar Sep 20 '22 22:09 RyanCavanaugh

@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?

ascott18 avatar Sep 21 '22 16:09 ascott18

I missed that - no, that's super weird

RyanCavanaugh avatar Sep 21 '22 18:09 RyanCavanaugh

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

Andarist avatar Oct 12 '22 12:10 Andarist

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

RyanCavanaugh avatar Oct 13 '22 18:10 RyanCavanaugh

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

Andarist avatar Oct 13 '22 18:10 Andarist