microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.82k stars 12.46k forks source link

Equivalent arguments typed with tuples aren't assignable to the same functions with overloads when the same function is recursively referenced #45466

Open niieani opened 3 years ago

niieani commented 3 years ago

Bug Report

🔎 Search Terms

arguments, overloads, function, tuple

🕗 Version & Regression Information

All versions with support for tuple arguments (upto v4.5.0-dev.20210815).

⏯ Playground Link

Playground link with relevant code

💻 Code

type START = 0
type DATA = 1

type Args<In, Out> =
  | [t: START, other: FnWithArgs<Out, In>]
  | [t: DATA]

type FnWithArgs<In, Out> = (...args: Args<In, Out>) => void

interface FnWithOverloads<In, Out> {
  (t: START, other: FnWithOverloads<Out, In>): void;
  (t: DATA): void;
}

declare const withArgs: FnWithArgs<1, 2>

// these should be assignable to one another, but aren't:
const assertion: FnWithOverloads<1, 2> = withArgs

🙁 Actual behavior

Two types that should be functionally equivalent aren't assignable to one another:

Type 'FnWithArgs<1, 2>' is not assignable to type 'FnWithOverloads<1, 2>'.
  Types of parameters 'args' and 't' are incompatible.
    Type '[t: 0, other: FnWithOverloads<2, 1>]' is not assignable to type 'Args<1, 2>'.
      Type '[t: 0, other: FnWithOverloads<2, 1>]' is not assignable to type '[t: 0, other: FnWithArgs<2, 1>]'.
        Type at position 1 in source is not compatible with type at position 1 in target.
          Type 'FnWithOverloads<2, 1>' is not assignable to type 'FnWithArgs<2, 1>'.
            Types of parameters 't' and 'args' are incompatible.
              Type 'Args<2, 1>' is not assignable to type '[t: 0, other: FnWithOverloads<1, 2>]'.
                Type '[t: 1]' is not assignable to type '[t: 0, other: FnWithOverloads<1, 2>]'.
                  Source has 1 element(s) but target requires 2.

I've tried simplifying the example, and this issue doesn't seem to occur when the parameters of the function don't reference themselves in their signature.

🙂 Expected behavior

No error, types should be assignable to one another.

Related issues

RyanCavanaugh commented 3 years ago

The example as presented is undecidable, thus rejected. Demonstrated:

type START = 0
type DATA = 1

type Args<In, Out> =
  | [t: START, other: FnWithArgs<Out, In>]
  | [t: DATA]

type FnWithArgs<In, Out> = (...args: Args<In, Out>) => void

type FnWithOverloads<In, Out> = {
  (t: START, other: FnWithOverloads<Out, In>): void;
  (t: DATA): void;
}

declare const withArgs: FnWithArgs<1, 2>
// Demonstrated: legal call
withArgs(0, withArgs);

// Asserted: This should be legal (use 'as any')
const assertion: FnWithOverloads<1, 2> = withArgs as any;
// Illegal call: 'withArgs' is not a substitute for 'assertion'
assertion(0, withArgs);

The last line is legal if and only if withArgs is substitutable for assertion, but that's the proposition we're testing when determining whether or not that's the case.

niieani commented 3 years ago

I'm sorry @RyanCavanaugh, I'm having trouble comprehending your example. In my eyes, it further demonstrates that the issue I described exists, i.e. assertion(0, withArgs) is illegal.

I understand that resolving In and Out is undecidable (because it recurses infinitely), however, my claim here is that at least the tuple/overload assignment should be legal, because both functions may be called only with an equivalent set of arguments. In other words:

// this is ok:
assertion(0, assertion);

// this is also ok:
withArgs(0, withArgs);

// and this SHOULD be okay, but isn't:
withArgs(0, assertion);

// the other way SHOULD also be okay (your example):
assertion(0, withArgs)
RyanCavanaugh commented 3 years ago

It seems I've overthought this. The simplest form doesn't work:

type Overloaded = {
  (n: number): void;
  (s: string): void;
}
type Tupled = {
  (...args: [number] | [string]): void;
}
let x: Overloaded = null as any as Tupled;
let y: Tupled = null as any as Overloaded;
fatcerberus commented 3 years ago

Are those actually equivalent? I ask because:

const stuff: [number] | [string] = [ 812 ] as any;
tupled(...stuff);  // okay
overloaded(...stuff);  // error!

TS Playground

The root of the issue seems to be that TS wants to be able to "choose" a specific overload (even though it's going to be the same underlying function being called at runtime), while this decision doesn't need to be made for the tuple case. So they are not equivalent, even though they are.

RyanCavanaugh commented 3 years ago

The root of the issue seems to be that TS wants to be able to "choose" a specific overload

This is a correct analysis of what's going wrong, but also a frequent source of complaint. Barring the problems of combinatorial explosion, this "should" work but doesn't.

niieani commented 3 years ago

Thanks @RyanCavanaugh, this is exactly the issue I was referring to! I noticed the label "Awaiting More Feedback", may I ask what type of feedback is expected? Or does this refer to some internal process?

andrewbranch commented 3 years ago

It means we’d like to see how many people 👍 this issue and comment with other solutions and ideas before considering it more.