Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Types for functions like Array.prototype.reduce or RxJS's "scan" seem impossible #46438

Open
benlesh opened this issue Oct 20, 2021 · 8 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@benlesh
Copy link

benlesh commented Oct 20, 2021

Bug Report

I searched, and couldn't find a related issue (but I find it very unlikely one doesn't exist). It seems like there should be something labeled "Design Limitation" or the like?

The basic problem is if you're using a standard "reducer" callback pattern with a seed that is different from what your reducer returns the returned type is incorrect.

// The type of result is `number`, but should be `string`.
const result = [1, 2, 3].reduce((acc, value) => {
//                              ~~~~~~~~~~~~~~~~~
//                                 ^-- Error Here: "No overload matches this call"
    if (acc === null) {
        return '' + value;
    } else {
        return acc + ', ' + value;
    }
}, null);

console.log(result); // "1, 2, 3"
console.log(typeof result); // "string"

🔎 Search Terms

  • is:issue is:open array reduce
  • is:issue is:open reduce callback
  • is:issue is:open reduce "No overload matches this call"
  • is:issue is:open reduce label:"Design Limitation"

🕗 Version & Regression Information

Any version of TS, to TMK. Specifically tested in 4.2 and 4.4.4

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries as best I could. There's apparently a LOT of them. I didn't see anything related.

⏯ Playground Link

An example in the playground

💻 Code

// The type of result is `number`, but should be `string`.

const result = [1, 2, 3].reduce((acc, value) => {
//                              ~~~~~~~~~~~~~~~~~
//                                 ^-- Error Here: "No overload matches this call"
    if (acc === null) {
        return '' + value;
    } else {
        return acc + ', ' + value;
    }
}, null);

console.log(result); // "1, 2, 3"
console.log(typeof result); // "string"

🙁 Actual behavior

A type error, even though the compiled JavaScript is valid.

🙂 Expected behavior

No type error, result should be a type string, not number.

Related:

ReactiveX/rxjs#6649

@andrewbranch andrewbranch added the Duplicate An existing issue was already created label Oct 20, 2021
@andrewbranch
Copy link
Member

@benlesh there are a few mentions of reduce in #36554, but I would encourage you to copy this example over to there as well for more coverage. Thanks!

@DanielRosenwasser
Copy link
Member

Our inference process is so fundamentally different here, but it does feel bad that when we have plain local statements, we're able to dive into the body of this loop and know it's either been set to string or null.

function foo(xs: number[]) {
    let acc = null;
    for (const value of xs) {
        if (acc === null) {
            acc = "" + value;
        }
        else {
            acc = acc + ", " + value;
        }
    }
    return acc;
}

I think at some point, I would like to pick @ahejlsberg's brain to understand how control flow analysis deals with these sorts of circularities (it's been a while for me, so I've largely forgotten). Maybe we could brainstorm on something that would work better with higher order functions like these. One issue with our inference process is that in our first pass going through arguments, we "lock in" inferences from expressions that aren't "contextually sensitive" (e.g. callbacks). The null is the only non-contextually sensitive thing here, so it wins. We could play around with tweaks to this process, but

  • it would be more complex
  • it might be a lot more work on every overload
  • it might break a huge chunk of code that exists today

@typescript-bot
Copy link
Collaborator

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

@benlesh
Copy link
Author

benlesh commented Oct 25, 2021

Yeah. I'm familiar with the approach of @nmain. However, I would strongly advise people that they should leverage inference and not pass explicit generics. I say this because generics are just another element of an API signature that can change over major versions. So reduce((acc: null | string, item) => would have been my go-to instead of <null | string>. That means the generics are implicit and you don't have to worry if they happen to change in a release.

@benlesh
Copy link
Author

benlesh commented Oct 25, 2021

Honestly, @DanielRosenwasser, I would rather appreciate this specific issue remain open, because I feel that this issue is unique to a reduce-type callback situation. As we see it in RxJS's scan, which clearly has nothing to do with Array methods. It's just impossible to get inference working right here, and I've literally seen hundreds of examples of people shooting themselves in the foot with explicit types that weren't accurate to accommodate this.

@andrewbranch andrewbranch reopened this Oct 26, 2021
@andrewbranch andrewbranch added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript and removed Duplicate An existing issue was already created labels Oct 26, 2021
@c69-addepar
Copy link

@benlesh are you asking for automatic union type promotion ?

Because in the example shown - there is nothing impossible here. Type error can be trivially fixed by the user:

  • replacing default value with a '' (empty string), instead of null - so all three types (accumulator, return, default value) would be the same, and the whole thing gets a nice signature reduce<A, T>((a: A, v: T) => A, default: A.
    • of course, resulting the need to rewrite the code in function body that depends on the null. This is not a problem for example you shown (`if(!acc)'), but could be trickier for more complicated real code. Still - the original problem is in using two types where there should have been one.

https://www.typescriptlang.org/play?#code/PTAEBUAsFNQFwJ4AdYHsBmoBO0DOBXAGzlAEtdQADAO3wFsAjaLSgGlAfxN0lSIBMOsSrjhZS1AOaUAdAFgAUIoDGqaqOx4iJALygA2gEZ2AJnYBmALoyc-fMugAKRwENly9gDcXhfNACUoDoAfKAA3oogoNExsXHxCdEAfimpaekpkWCJOblxAHoAtIWgAKJYWKhYoAASzNAAXKAARAByqKConsyEqC6CdC5wyjAUcJDkoMo+hM2KsaSYru5BOnq0hISBEQqJOHD4WNSgAOQnoADUoN6+0ADc8zEAvqDQhLiwO3vQB0egbspLqd2Ocrjc-A9ds9FE8QSd-JCVGpcKhCNAZL1JI4cARiAjQFFmsZQGZQOY5gpVOpUejMY5ECgMJpcXB8YTROIpM0gA

ps: i understand that many people would not agree on abandoning the use of null for default values, so those ones can type , null as null|string); at the end of their reduce/scan function call.

const result = [1, 2, 3].reduce((acc, value) => {
    if (acc === null) {
        return '' + value;
    } else {
        return acc + ', ' + value;
    }
}, null as null|string);

How can (and why should) Typescript promote null to a union type - that's another question, though.. reduce<A,T,D>(a:A|D, v:T) => A, default: A|D), looks pretty messy to me already, and if such promotion will be special-cased to null only (via conditional types), then it becomes almost magical (in a bad sense). Yet, if this will help people to describe their code more accurately - then maybe this is the way to go 🤷

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants