Overload call signature resolution resolves to first overload
🔎 Search Terms
overload, multiple call signature
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about overloads
⏯ Playground Link
https://www.typescriptlang.org/play/?ts=5.5.2&ssl=15&ssc=1&pln=1&pc=1#code/JYOwLgpgTgZghgYwgAgHIHswG8CwAoZQ5ACgA8AueAGwGcIBKcsKAVxQHp3kbgBzEOGBZQUARnxESFZm0bU6yTtz4ChI5ACZ8EomXIAjdOioQ4IRoeOmQirj36DhKAMwA6ZAFUQCE3CjIAdwALCDAQ-zDgGm4g9BYqABNkfRQEiB8-CCT0fxBMfABffDSM9QA3P2Q8sHIMMHwEdBAaMCrMAEEQAE9kAF42sGIZFDhosy76W2RhgG5kllaaWPiklOQAA0tfEHX8JSIAPQB+bTwwLoAHFAANPuQAJVDhEAAVS4gAHjqAPimt6zm8hQwBgygcahcyCiyHQAFtgGBIAk9lxCMcGk0WgMAEJGbZ3apDVgjaL-MyTJRkkBzaBQHJQ0H2VROZDOKHROEIpEoyTovBAA
💻 Code
interface Not{
(x:false):true // signature 1
(x:true):false // signature 2
(x:boolean):boolean // signature 3. Unclear whether this should be declared or not
}
declare var not:Not
const notAny = not(true as any) // true; but should be `boolean`
// ^?
type X = ReturnType<Not> // boolean; false if signature 3 is omitted
// ^?
const notBoolean = not(true as boolean) // boolean; error if signature 3 is omitted
// ^?
🙁 Actual behavior
x types as true, the first signature of the overload.
🙂 Expected behavior
x should be boolean, the return type of signature 3.
Additional information about the issue
Note that #14107 would obviate the need for signature 3.
Originally discovered based on PR Feedback.
It is unclear to this author whether the implementation signature belongs in a pure declaration.
The FAQ indicates the implementation signature will typically not be externally visible:
When having at least one overload signature declaration, only the overloads are visible. The last signature declaration, also known as the implementation signature, does not contribute to the shape of your signature.
Previous discussion suggests the "catch-all" should actually be visible for consumers:
The intended thing for users to do is ... Have a "catch-all" overload, last, that represents the behavior of the function when overload resolution is ambiguous, whose return type should be the union of all other signatures' return types
You forgot to fill out the issue template.
type X = ReturnType<Not> // boolean; false if signature 3 is omitted
This is working as intended and documented: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types
When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferences are made from the last signature (which, presumably, is the most permissive catch-all case).
It is unclear to this author whether the implementation signature belongs in a pure declaration.
Implementation signatures can't be part of interfaces because interfaces don't have implementations. Whether you should have a permissive "catch all" signature as part of your interface or not really depends on your use case.
The only weird thing here is that true as any resolves to the true overload, but I feel I've seen this in another issue already.
Implementation signatures can't be part of interfaces because interfaces don't have implementations. Whether you should have a permissive "catch all" signature as part of your interface or not really depends on your use case.
Yes, I'm confusing the terms "implementation signature" and "catch all signature". It seems like catch-all signatures are a workaround inspired by implementation signatures. And they would largely not be needed if function overload distributed over union.
The only weird thing here is that
true as anyresolves to thetrueoverload, but I feel I've seen this in another issue already.
That's my understanding too. Maybe a duplicate of #39833?
Yes, I'm confusing the terms "implementation signature" and "catch all signature".
function foo(v: 1): void;
function foo(v: 2): void;
function foo(v: 1 | 2): void; // catch all overload, just another overload signature that's visible to the caller
function foo(v: 1 | 2): void { // implementation signature, not visible for caller
// implementation be here
}
And they would largely not be needed if function overload distributed over union.
No idea what you mean. This behavior is intentional.
That's my understanding too. Maybe a duplicate of #39833?
Yes, that's the one.
@MartinJohns By distribution over unions I think he means that given the overloads...
declare function foo(v: 1): void;
declare function foo(v: 2): void;
...it should be legal to call foo(v) with a v typed as 1 | 2. Which comes up semi-regularly and I'm almost positive there's at least one open issue about it. My understanding was that the reason that isn't done is for performance reasons because it becomes combinatorially explosive in the number of parameters and union members.
@MartinJohns By distribution over unions I think he means that given the overloads...
declare function foo(v: 1): void; declare function foo(v: 2): void;...it should be legal to call
foo(v)with avtyped as1 | 2. Which comes up semi-regularly and I'm almost positive there's at least one open issue about it. My understanding was that the reason that isn't done is for performance reasons because it becomes combinatorially explosive in the number of parameters and union members.
Exactly. In this your example, it simplifies out to void. In general, the return types of chained calls will tend to unknown or any. Only if the output types are disjoint will you get the combinatorial growth (and it’s a sum not the product behavior of generic or template string types!).
The combinatorial behavior I’m talking isn’t about the return types, but how much work has to be done to typecheck the call sites. Basically if there are multiple arguments, each with a union type, you’d have to take each union apart and, for each member, check if it matches one of the overloads. That’s on average exponential in the number of arguments provided.
The combinatorial behavior I’m talking isn’t about the return types, but how much work has to be done to typecheck the call sites. Basically if there are multiple arguments, each with a union type, you’d have to take each union apart and, for each member, check if it matches one of the overloads. That’s on average exponential in the number of arguments provided.
I’m still not seeing it. Could you give a concrete example?
Suppose you have
declare function foo(x: A, y: A): void;
declare function foo(x: A, y: B): void;
declare function foo(x: B, y: A): void;
declare function foo(x: B, y: B): void;
declare let x: A | B;
declare let y: A | B;
foo(x, y);
This call should succeed according to the types. But to know that, you have to answer all of these in the affirmative:
- Is
foo(A, A)a valid call? - Is
foo(A, B)a valid call? - Is
foo(B, A)a valid call? - Is
foo(B, B)a valid call?
and each of those individual checks has to check all the overloads for a match. Now increase the size of the unions and/or the number of parameters and this very quickly gets out of hand.
- That's not a combinatorial explosion in checking difficulty - it's one case per overload signature. Yes, it would require you to declare combinatorially many cases, but if the function can be factored into a single signature, it makes sense to do so (
foo(x:A|B, y:A|B): void). If not, then the most general signature is awkward anyway. - TypeScript already allows assigning a tuple of union types to a union of tuple types and vice versa. That's morally the same process:
declare let x: [A,A] | [A,B] | [B,A] | [B,B] declare let y: [A|B, A|B] x = y
it's one case per overload signature
It's not though - if all the overloads aren't there, it might be able to short-circuit once it finds a failing case, but in principle it still has to enumerate all the possible combinations of union members. The "40 questions" bit I described would still happen, even if the matching overloads aren't there.
TypeScript already allows assigning a tuple of union types to a union of tuple types and vice versa.
Indeed, but IIRC there's already a (fairly low) limit to the size of the unions that can be expanded out during checking that way before it just fails. Something like 25 if my memory is right.
it's one case per overload signature
It's not though - if all the overloads aren't there, it might be able to short-circuit once it finds a failing case, but in principle it still has to enumerate all the possible combinations of union members. The "40 questions" bit I described would still happen, even if the matching overloads aren't there.
Okay I think I understand. You're assuming the implementation will be "normalize the types of both the arguments and the parameters to unions of tuple types and check that every argument case is covered by a parameter case". This is a really natural approach to take - canonicalizing to a sum (union) of products (Cartesian product).
If so, then yes, it is an expensive check! But that assumes the worst-case scenario. You don't need to eagerly expand out the argument nor the parameter types into unions. Keep the argument type as a tuple and only split into cases when an overload partially matches the argument type.
e.g. for arguments [A, B] and an overload with parameters [D, E], the case [A&D, B&E] is handled by the overload. You then need to check [Exclude<A,D>, B] | [A, Exclude<B,E>] against the other overloads. If A is disjoint from D, it simplifies to [A,B]. And if A is a subset of D, it simplifies to [A, Exclude<B,E>].
You could also try to gather the argument type into a product of sums (e.g. foo) but I expect that's not very useful in practice - if the function can be declared that way, it probably is.
TypeScript already allows assigning a tuple of union types to a union of tuple types and vice versa.
Indeed, but IIRC there's already a (fairly low) limit to the size of the unions that can be expanded out during checking that way before it just fails. Something like 25 if my memory is right.
Seems like you're right! And it doesn't even give a meaningful error message! This does strongly suggest that a casewise analysis like the above is being used in the code. And that it's an opportunity for optimization.
Overload resolution selects the first matching signature (everyone loves it when language use simple algorithms, right? right??). There have been requests to do something more involved when any is involved, but none of them have really succeeded in staking out a definition doesn't break huge swathes of user code; many times overloads are arranged with a "common" case first and an "uncommon" case second and people want the "common" one to be selected in an any invocation.
The one caveat to "first" is that there's an initial pass that tries to discard assignments to any, so if you really need your interface to do a particular thing on any, you can declare that without breaking more-specific inferences:
interface Not{
(x: any): boolean;
(x:false):true // signature 1
(x:true):false // signature 2
(x:boolean):boolean // signature 3. Unclear whether this should be declared or not
}
declare var not:Not
const p = not(true);
// ^? still 'false'
The one caveat to "first" is that there's an initial pass that tries to discard assignments to any, so if you really need your interface to do a particular thing on any, you can declare that without breaking more-specific inferences
Clever workaround, but that defeats the type annotation on the argument. Somehow the only workaround that seems to work is to make it a generic function:
interface Not2 {
<const T extends boolean> (x:T) : T extends true ? false : T extends false ? true : never
}
Overload resolution selects the first matching signature.
Somehow though, adding (or moving) the catch-all signature first results in matching the second overload, with weird results:
interface Not{
(x: boolean): boolean;
(x: false): true;
(x: true): false;
(x: boolean): boolean;
}
declare var not:Not
const notAny = not(true as any) // true !?!?
// ^?
It seems in intellisense that the boolean-accepting overloads are being shuffled to number 3 and 4 in the list!
This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.