Skip to content

Still only the last overloaded signature is picked when passing an overloaded function to Array.map #61398

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

Closed
ExplodingCabbage opened this issue Mar 11, 2025 · 11 comments
Labels
Duplicate An existing issue was already created

Comments

@ExplodingCabbage
Copy link

ExplodingCabbage commented Mar 11, 2025

πŸ”Ž Search Terms

map overload overloaded

πŸ•— Version & Regression Information

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

⏯ Playground Link

https://www.typescriptlang.org/play/?noImplicitAny=false#code/GYVwdgxgLglg9mABMOcAUAPAXIsIC2ARgKYBOAlDnkWQNwBQoksCyqmOAzlKTGAOaVE3XgIZNo8JCnQZyiAN71EKxKWJQQpJBgYBfevQgJuiAIalSVAiVIBtALqIAvIjsBGADQAmTwGYHADp8MwAHNBlyWiA

Bug report

This code fails to compile:

function foo(x: number): number;
function foo(x: string): string;
function foo(x) {
    return x;
}

const arr: number[] = [1,2,3].map(foo);

with the following errors:

Type 'string[]' is not assignable to type 'number[]'.
  Type 'string' is not assignable to type 'number'.

and

Argument of type '{ (x: number): number; (x: string): string; }' is not assignable to parameter of type '(value: number, index: number, array: number[]) => string'.
  Type 'number' is not assignable to type 'string'.

This doesn't make much sense, since there's no reason (so far as I can see) that TypeScript shouldn't be able to recognise that foo will only be passed numbers as arguments when called via [1,2,3].map(foo);, and that therefore (per the two overload signatures) will only return numbers, not strings. Furthermore, it can in fact infer this if you use an arrow function instead of passing foo as an argument directly, i.e. this compiles just fine:

function foo(x: number): number;
function foo(x: string): string;
function foo(x) {
    return x;
}

const arr: number[] = [1,2,3].map(x => foo(x));

Especially odd is that the original code also compiles fine if you swap the order in which the overloads are defined:

function foo(x: string): string;
function foo(x: number): number;
function foo(x) {
    return x;
}

const arr: number[] = [1,2,3].map(foo);

The explanation appears to me to be that posited by #55840 - that, when an overloaded function is passed as a parameter to a function like Array.map, TypeScript simply (and arbitrarily, and incorrectly) treats its final overload signature as the signature of the function. It should instead infer from context which overload signature is applicable, as it successfully does if you wrap the call in an arrow function.

#55840 was closed as a dupe of #47571, which was closed as having been completed, and indeed the specific example given in that issue no longer seems to reproduce the bug (so I guess that something got changed to fix at least that case), but the underlying bug doesn't seem to have truly been fixed since the trivial example I give at the start of this issue still reproduces it.

@MartinJohns
Copy link
Contributor

of #47571, which was closed as having been completed

It's tagged as a design limitation.

@ExplodingCabbage
Copy link
Author

@MartinJohns hmm, so it is. But is that either 1. actually accurate or 2. the reason for closure, which happened years later on the grounds that the issue was completed (not that it wouldn't be fixed)?

I don't see an explanation there of why such a design limitation exists, and naively it seems like there shouldn't be such a limitation; if TypeScript can do the inference correctly for the arrow function example, why not the basically-equivalent one where foo is passed directly?

@jcalz
Copy link
Contributor

jcalz commented Mar 11, 2025

EDIT: Sorry, somehow the prev 2 comments did not show up for me until after I posted this

indeed the specific example given in that issue no longer seems to reproduce the bug

Are you sure? Still looks like it picks the last overload to me. #47571 was a design limitation and closed presumably because there's nothing to be done, it's a design limitation.

I can't pretend to speak authoritatively about why they can't resolve overloads for callbacks, but presumably it would trigger a lot of extra analysis which could snowball in unmanageable ways. I'd think such an explanation is in some GitHub issue somewhere, or maybe someone can add such an explanation here?

@MartinJohns
Copy link
Contributor

the reason for closure, which happened years later on the grounds that the issue was completed (not that it wouldn't be fixed)?

At some point the team started to close all issues where no work is to be done anymore. Don't pay attention to the closing status of GitHub, pay attention to the tags added by the team and comments if there are any.

@jcalz
Copy link
Contributor

jcalz commented Mar 11, 2025

#52944 and #53057 are relevant? Looks like something was implemented and there was a big performance hit for material-ui, and I can't pretend to understand everything in the design meeting summary.

@ExplodingCabbage
Copy link
Author

Are you sure? Still looks like it picks the last overload to me.

Huh, I must've done something wrong when I tested this locally; it seems it still does this in the playground on the latest version.

#47571 was a design limitation ... I can't pretend to speak authoritatively about why they can't resolve overloads for callbacks, but presumably it would trigger a lot of extra analysis which could snowball in unmanageable ways

I guess I'm sceptical of this for two reasons:

  1. Though I definitely might be missing something, this seems to me like it'd be fixable or partly-fixable in principle just with a preprocessing step that roughly rewrites calls like f(g) into f((a,b,c) => g(a,b,c)) (in the case where g is typed as a function and f expects a function as first parameter). Obviously there are some annoying nuances that make this not totally trivial (determining how many arguments the arrow function should take would require considering the signatures of f and g, and there'd be some annoying stuff involving this that I'm not considering) but nothing fundamentally impossible. The fact that the version of the code with arrow functions works fine shows that the underlying logic to do this sort of inference already exists and works, and it only takes an in-theory-automatable refactor to convert my currently non-compiling code into equivalent code that triggers that inference.

  2. Even if there is some reason none of us can yet see that proper inference isn't possible, the approach of using the final overload is completely arbitrary. It would be better - less fragile, less confusing - to just always emit an error in circumstances where inference can't be used and TypeScript is having to resort to arbitrarily using the last overload. (That error could advise rewriting with an arrow function to make inference work.)

@jcalz
Copy link
Contributor

jcalz commented Mar 11, 2025

They're not going to always emit an error and break a bunch of real world code. The last overload is chosen because it is most likely to be the least restrictive, catch-all signature (like, it's the lowest priority signature so it should only be chosen after all prev signatures have failed). It's not completely arbitrary. Of course overloaded functions aren't required to have a catch-call signature, so for the ones that don't, choosing the last overload isn't any more helpful than choosing one at random.

Anyway this behavior is what it is, has been for a long time, and many issues have been raised and closed about it. They're not going to just change it here. At best you'll get an explanation for why it is this way, which I'm guessing has to do with performance. Since I'm mostly just speculating and repeating myself I'll bow out now. Good luck!

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Mar 14, 2025
@RyanCavanaugh
Copy link
Member

Why are design limitations closed -> https://github.com/Microsoft/TypeScript/wiki/FAQ#this-is-closed-but-should-be-open-or-vice-versa

@ExplodingCabbage
Copy link
Author

@RyanCavanaugh so, just to be clear, the intended reason for closure is that the team doesn't know how to fix it, not that it's completed as per the closure status? Let's post something to that effect on the issue, then.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 14, 2025

GitHub added the "why was this closed (completed/not done)" bit after we closed a bunch of stuff, and auto-marked a bunch of stuff as "completed" as a result. Also for a long time, there was no way from the API to specify the close reason, so anything closed via automation has the wrong status. As a result I would really love it if people could just ignore that bit; I don't have the time nor mental capacity to go back through 40,000 issues and validate their "close reason" post facto.

@ExplodingCabbage
Copy link
Author

Cool, I've written up a bulleted summary of my understanding of the posts above at #47571 for the benefit of anyone else landing there from search.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants