Skip to content
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

Inconsistent type widening with results of higher-order functions #51551

Closed
johanrosenkilde opened this issue Nov 16, 2022 · 4 comments
Closed
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@johanrosenkilde
Copy link

Bug Report

When assigning the value 0 or 1 to the field of an object, see below, type widening infers the type number. When doing the same through a generic higher-order function, the field gets the type 0 | 1. This can trigger a type error when using that value in another generic high-order function that assigns to the object as a side-effect.

🔎 Search Terms

type widening
inconsistency

🕗 Version & Regression Information

Appears in all versions on Playground (back to 3.3.3333).

Also appears in Nightly (v. 5.0.0.-dev20221116)

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

⏯ Playground Link

Playground link with relevant code

💻 Code

// The following example works by type widening

function wrap<L>(v: L): { val: L } {
    return { val: v };
}

export function thisworks(): number {
    const g = (b: boolean) => { return b ? 1 : 0; }
    let o = wrap(g(true));
    o   /// Type: o: { val: number }
    o.val += 1;
    // o.val : number
    return o.val;
}

// The following example does exactly the same but combines `wrap` and `g`
// into a single call to a high-order function.
// Now type-widening doesn't apply.

function applyWrap<L>(b: boolean, map: (b: boolean) => L): { val: L } {
    return { val: map(b) };
}

function mapVal<L>(o: { val: L }, map: (arg: L) => L): void {
    o.val = map(o.val);
}

export function doesntwork(): { val: number } {
    let o = applyWrap(true, (b: boolean) => { return b ? 1 : 0; });
    o   /// Type: o: { val: 0 | 1 }
    // The next line triggers an error by again preventing type widening in the assignment
    mapVal(o, v => { return v + 1; });
    // This line would have worked:
    // o.val += 1
    return o;
}

🙁 Actual behavior

I get a type error in the function doesntwork on the mapVal call. This is because type widening did not apply as expected to o, so it has type { val: 0 | 1 }. Simultaneously, type widening is also disabled at the mutation mapVal -- simply writing o.val += 1 instead of the mapVal call would have worked.

🙂 Expected behavior

I expected that the behaviour of type widening remains the same in the case of using high-order callbacks to generate the value and not.

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Nov 17, 2022
@RyanCavanaugh
Copy link
Member

The widening rules are in place to heuristically attempt to infer "better" types and different code is allowed to behave differently under these heuristics

Re increment, see #14745 (comment)

I expected that the behaviour of type widening remains the same in the case of using high-order callbacks to generate the value and not.

When specifying expected behavior, "the same" isn't really actionable. "These two slightly different things should behave identically" quickly runs into transitivity problems -- A1 is close to A2 is close to A3 is close to A4 so each step should be "the same", but if A1 and A4 have manifestly different desirable behaviors, then something has to give somewhere.

@johanrosenkilde
Copy link
Author

Understood, that type widening is a heuristic that tries to balance psychological expectancy with precise semantics, and that there will be unboundedly many special cases you could consider.

For this issue, I'd expect type widening to apply in the same way because locally, all precisely-determined types on right-hand-sides in the doesntwork function are the same as in the thisworks function. All that differs is the way the values and types came about. But I readily acknowledge ignorance of the way type widening works on a deeper level and in implementation, and that there may be subtleties I'm not seeing.

@fatcerberus
Copy link

fatcerberus commented Nov 18, 2022

For this issue, I'd expect type widening to apply in the same way because locally, all precisely-determined types on right-hand-sides in the doesntwork function are the same as in the thisworks function. All that differs is the way the values and types came about.

This sounds like equational reasoning (e.g. when id 42 can always be substituted for 42 and vice versa, without changing semantics), which doesn't really hold in general in TypeScript. Which is admittedly frustrating at times, but them's the breaks.

@github-actions
Copy link

github-actions bot commented Jun 8, 2023

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

@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Jun 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

3 participants