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

Inconsistent inferred overloads in conditional types for no-arg signatures with strictFunctionTypes #28867

Closed
jcalz opened this issue Dec 5, 2018 · 3 comments · Fixed by #54448
Assignees
Labels
Domain: Conditional Types The issue relates to conditional types Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@jcalz
Copy link
Contributor

jcalz commented Dec 5, 2018

TypeScript Version: 3.3.0-dev.20181205

Search Terms:
conditional type, infer, overload, no-arg, zero arguments, strictFunctionTypes

Note that this issue is not about the behavior where the last overload is examined in conditional type inference of a single signature, where some might expect a union or an argument-based choice.

Code

// turn on --strictFunctionTypes

type InferTwoOverloads<F extends Function> = 
  F extends { (...a1: infer A1): infer R1, (...a0: infer A0): infer R0 } ? 
  [(...a1: A1) => R1, (...a0: A0) => R0] : 
  never;

type Expected = InferTwoOverloads<((x: string) => number) & (() => string)>
// [(x: string) => number, () => string]    

type JustOneSignature = InferTwoOverloads<((x: string) => number)>;
// never

type JustTheOtherSignature = InferTwoOverloads<(() => string)>;
// [(...a1: unknown[]) => {}, () => string]

Expected behavior:
I would expect that either both JustOneSignature and JustTheOtherSignature would be never, or both JustOneSignature and JustTheOtherSignature would evaluate to a two-tuple where the first signature is... something. I suppose (...a1: unknown[])=>{}? Or maybe it should be a copy of one of the other overloads? Or something in between?

Actual behavior:
It looks a zero-argument function is being treated pathologically during conditional type inference; if a type is callable with zero arguments, it will be considered assignable to any number of overloads with what looks like failed inference for the types of the arguments (unknown[]) and the return ({}). Otherwise, if a type is only callable with at least one argument, it will only be considered assignable to the matching number of overloads.

This distinction only seems to happen with --strictFunctionTypes on. If you turn it off, JustOneSignature above becomes [(...a1: unknown[]) => {}, (x: string) => number], which is at least consistent with the zero-argument situation.

Playground Link:
🔗

Related Issues:

@weswigham weswigham added Needs Investigation This issue needs a team member to investigate its status. Domain: Conditional Types The issue relates to conditional types labels Dec 5, 2018
@sirian
Copy link
Contributor

sirian commented May 13, 2019

As i mentioned in duplicate issue - problem not only in no arg function. also with (x: unknown) => any, (x: any) => any, (...args: unknown[]) => any etc.

type Overloads<F> =
    F extends {
          (...args: infer A1): infer R1
          (...args: infer A2): infer R2;
      } ? {rule: 2, variants: [A1, R1] | [A2, R2]} :
    F extends {
          (...args: infer A1): infer R1;
      } ? {rule: 1, variants: [A1, R1]} :
    never;

declare const ok1: Overloads<(x: number) => boolean>;
// {rule: 1, variants: [[number], boolean]}

declare const ok2: Overloads<{(): 1; (x: number): 2}>;
// {rule: 2, variants: [[], 1] | [[number], 2]}

declare const wrong1: Overloads<() => boolean>;
// {rule: 2, variants: [[], boolean] | [unknown[], {}]}
// expected {rule: 1, variants: [[], boolean]}

declare const wrong2: Overloads<(...args: unknown[]) => boolean>;
// {rule: 2, variants: [unknown[], boolean] | [unknown[], {}]}
// expected {rule: 1, variants: [unknown[], boolean]}

declare const wrong3: Overloads<(x: unknown) => boolean>;
// {rule: 2, variants: [[unknown], boolean] | [unknown[], {}]}
// expected {rule: 1, variants: [[unknown], boolean]}

declare const wrong4: Overloads<(x: any) => boolean>;
// {rule: 2, variants: [[any], boolean] | [unknown[], {}]}
// expected {rule: 1, variants: [[any], boolean]}

@jcalz
Copy link
Contributor Author

jcalz commented Nov 8, 2019

Relevant SO question: How to use Parameters type on overloaded functions?

@ahejlsberg
Copy link
Member

When inferring between types with multiple signatures, we match the signatures pairwise from the bottom up. This is a fine strategy when the source and target have matching arities (number of signatures) and when the source arity is greater than the target arity. But when the source arity is less than the target arity we never make inferences to the excess signatures in the target. That is a suboptimal strategy because in the absence of inference candidates we infer the constraint (which in turn defaults to unknown) and that may end up producing very restrictive signatures.

We want to ensure that every signature in the target is matched with some signature in the source, so a better strategy is to infer between signatures pairwise from the bottom up and then infer from the first source signature to each of the excess target signatures. I will put up a PR to that effect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: Conditional Types The issue relates to conditional types Fix Available A PR has been opened for this issue Needs Investigation This issue needs a team member to investigate its status. Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants