Skip to content

Interface function of arity of zero gets inferred as having two different method signaturesΒ #53508

Description

Bug Report

πŸ”Ž Search Terms

overloads, infer, interfaces, arity

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about overloads

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

type Overloads<T> = T extends {
    (...args: infer A1): infer R1;
    (...args: infer A2): infer R2;
  } 
  ? [(...args: A1) => R1, (...args: A2) => R2]
  : T extends {
      (...args: infer A1): infer R1;
    } 
    ? [(...args: A1) => R1]
    : any;

interface A {
  hello(str: string): string
}
type HelloOverloadsOfA = Overloads<A['hello']>; // GOOD [(str: string) => string]

interface B {
  hello(): number
  hello(str: string): string
}
type HelloOverloadsOfB = Overloads<B['hello']>; // GOOD [() => number, (str: string) => string]

interface C {
  hello(): number
}
type HelloOverloadsOfC = Overloads<C['hello']>; // BAD [(...args: unknown[]) => unknown, () => number]

πŸ™ Actual behavior

When the interface in question contains a single implementation of a function, having arity zero, the Overloads utility type infers it as two different functions:

  • (...args: unknown[]) => unknown, and
  • () => number

πŸ™‚ Expected behavior

I expected Typescript to infer it as () => number alone. This would be consistent with how this utility type infers other methods having a single implementation, but arity > 0.

Activity

  1. changed the title [-]Function with arity of zero gets inferred as having two different method signatures[/-] [+]Interface function of arity of zero gets inferred as having two different method signatures[/+] on Mar 26, 2023
  2. RyanCavanaugh commented on Mar 28, 2023

    @RyanCavanaugh
    Member

    FWIW trying to do this (inferring from an overload set) in the first place is probably a very bad idea; I don't think anyone has ever done this before and it's really never been considered as a way someone might use infer.

    Anyway this inference follows from the inference algorithm: It's correct for C["hello"] to match against the 2-overload variant, since it can accept some possible instantiation of both overloads. It's correct to assign all of the candidates to A1 and R1, leaving zero candidates to assign to A2 and R2 (or vice versa), and at that point the other parametrs have zero candidates so fall back to unknown, and since C['hello'] has no parameters, it correctly can accept an unknown[]. The only plausible tweak would be to say that for overloads, the candidate-splitting logic shouldn't apply, but then you'd just get [() => number, () => number] which isn't really desirable in this type either.

    It's definitely undesirable but I don't see a way to change the rules here without breaking something else much more badly. If someone wants to put together a PR that shows otherwise, happy to review it, but I think the algorithm is broadly working as designed here.

  3. Andarist commented on Mar 29, 2023

    @Andarist
    Contributor

    FWIW trying to do this (inferring from an overload set) in the first place is probably a very bad idea; I don't think anyone has ever done this before and it's really never been considered as a way someone might use infer.

    I've done it here. I learned about this being possible from this monster.

    You know how it is... never say never 🀣

  4. steveluscher commented on Mar 29, 2023

    @steveluscher
    Author

    While we're showing-and-telling, here's my use case in case it inspires anything.

    I parse out overloads like this, then using them like this.

  5. steveluscher commented on Mar 29, 2023

    @steveluscher
    Author

    …since C['hello'] has no parameters, it correctly can accept an unknown[].

    Can you explain this to me, Ryan Cavanaugh (@RyanCavanaugh)? Normally, if I take a function that has no parameters (arity zero) and I try to call it with any parameters at all, that's a type error.

    function noArgs() {}
    
    noArgs(); // OK
    
    noArgs(123); // ERROR: Expected 0 arguments but got 1
    noArgs.call(undefined, 123); // ERROR Expected 1 argument but got 2
    
    noArgs(...([] as unknown[])); // ERROR A spread argument must either have a tuple type or be passed to a rest parameter.
    noArgs.apply(undefined, [] as unknown[]); // Target allows only 0 element(s) but source may have more

    In that light, why is (...args: unknown[]) => unknown a suitable overload of () => number?

    Playground link.

  6. Andarist commented on Mar 30, 2023

    @Andarist
    Contributor

    I think that what Ryan Cavanaugh (@RyanCavanaugh) meant here is that:

    type Zero = () => void
    type UnknownArgs = (...args: unknown[]) => void
    
    type Test = Zero extends UnknownArgs ? 1 : 0 // 1
    
    const fn: (arg: number) => void = () => {}
    
    // this call is OK, the type expects a number, and this is satisfied
    // it doesn't matter if the implementation uses this param or not
    // it won't crash on the surplus of arguments
    fn(100)
  7. steveluscher commented on Mar 31, 2023

    @steveluscher
    Author

    OK. I think I see what's going on here.

    Fact 1; we all agree on arity being enforced at the call level

    Nobody here disagrees that this should be a type error, because a call is being made that violates the arity of this function:

    function noArgs() {}
    noArgs(123); // ERROR: Expected 0 arguments but got 1

    Fact 2; assignability does not concern itself with arity

    Whether it's proper to call a function with a given number of arguments is one thing, but whether it should be assignable to a type is quite another. Just because this function only cares about one argument doesn't mean it shouldn't be assignable to a type that provides for more.

    function reportError(errorName: string) {}
    function logStuff(onError: (errorName: string, errorCode: number) => void) {
      onError('bad thing', 500);
    }
    
    logStuff(reportError); // OK. The callback will only make use of the `errorName`, but that's fine.

    Conclusion

    So given that everything is working, as designed, the upshot is that I'm just hosed unless #29732 gets implemented. Ultimately the reason that I've built this overload-inferring utility type is because I need to map over every overload to modify the return type of each. In short, what I want to accomplish is this:

    interface ThingDoer {
      doThing(): void;
      doThing(num: number): string;
      doThing(str: string): bigint;
    }
    
    type AsycThingDoer = Asyncify<ThingDoer>;
    // Produces:
    // {
    //   doThing(): Promise<void>;
    //   doThing(num: number): Promise<string>;
    //   doThing(str: string): Promise<bigint>;
    // }
    
    type Asyncify<T> = // Impossible with Typescript as it is designed, currently.
  8. Andarist commented on May 31, 2023

    @Andarist
    Contributor

    FWIW trying to do this (inferring from an overload set) in the first place is probably a very bad idea; I don't think anyone has ever done this before and it's really never been considered as a way someone might use infer.

    I'm not sure if this is the first test time that a test case like this is being introduced to the codebase but at the very least this PR "codifies" this as something that is being considered and one that should have some reliable semantics behind it.

  9. github-actions commented on Jun 8, 2023

    @github-actions

    This issue has been marked as 'Not a Defect' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  10. steveluscher commented on Mar 17, 2025

    @steveluscher
    Author

    This appears to be fixed (enough) in TypeScript 5.3.3.

  11. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Not a DefectThis behavior is one of several equally-correct options

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions