Skip to content

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

Open

Description

@benlesh

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

Activity

  1. andrewbranch commented on Oct 20, 2021

    @andrewbranch
    Member

    Ben Lesh (@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!

  2. DanielRosenwasser commented on Oct 20, 2021

    @DanielRosenwasser
    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 Anders Hejlsberg (@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
  3. typescript-bot commented on Oct 23, 2021

    @typescript-bot
    Contributor

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

  4. benlesh commented on Oct 25, 2021

    @benlesh
    Author

    Yeah. I'm familiar with the approach of Nicholai Main (@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.

  5. benlesh commented on Oct 25, 2021

    @benlesh
    Author

    Honestly, Daniel Rosenwasser (@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.

  6. added
    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.
    SuggestionAn idea for TypeScript
    and removed
    DuplicateAn existing issue was already created
    on Oct 26, 2021
  7. c69-addepar commented on Nov 2, 2021

    @c69-addepar

    Ben Lesh (@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 🤷

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

    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions