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

Object Literal Assignment Does Not Participate in Type Narrowing #46489

Closed
ericdrobinson opened this issue Oct 22, 2021 · 7 comments
Closed

Object Literal Assignment Does Not Participate in Type Narrowing #46489

ericdrobinson opened this issue Oct 22, 2021 · 7 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@ericdrobinson
Copy link

Bug Report

🔎 Search Terms

object literal definition type narrowing type guard

Possibly related: #16896 (especially this comment).

🕗 Version & Regression Information

This is the behavior in every version I tried, and I reviewed the FAQ for entries about 'object' and 'literal'

⏯ Playground Link

Playground link with relevant code

💻 Code

interface Test
{
    a?: number;
}

let x: Test = { a: 23 };

// Error: Type 'number | undefined' is not assignable to type 'number'. (2322)
const val: number = x.a;

🙁 Actual behavior

The compiler does not recognize that the type of property a should be narrowed to number due to the literal definition. This is always a surprise.

🙂 Expected behavior

I expect the type to be narrowed such that the property a is treated as "narrowed to number" until otherwise modified in the scope that follows the assignment.

This feels entirely natural as it's how most other assignments work. This comment puts it nicely:

Initializations are just treated as assignments. So this is basically const value: number | null; value = 123;, and no one would expect value to be possibly null right after assigning a number to it.

In this case, I do not expect x.a to be missing right after assigning to it an object literal with a defined.

@ericdrobinson
Copy link
Author

Frustratingly, this code works around the issue:

interface Test
{
    a?: number;
}

let x: Test = {};
x.a = 23;

// OK.
const val: number = x.a;

But having to break up object literal declarations feels like an unnecessary hoop for something the compiler could presumably detect.

We could also throw in a standard type guard, but that's just extraneous (both at runtime and in the code).

@andrewbranch
Copy link
Member

You’re right that the comment you linked to in #16896 is exactly this, but I think #16896 on the whole is not. The deal is that only union types narrow on assignment, which is why let x: number | string = "hello" becomes string right after being assigned. But Test is not a union type, so when you assign a value to it, nothing happens except the initializer is checked for conformance to Test. In your workaround, you have an assignment to x.a, which gets matched as a narrowing reference later. This is just a general limitation of the design of our control flow narrowing system.

@andrewbranch andrewbranch added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Nov 12, 2021
@ericdrobinson
Copy link
Author

The deal is that only union types narrow on assignment, which is why let x: number | string = "hello" becomes string right after being assigned. But Test is not a union type, so when you assign a value to it, nothing happens except the initializer is checked for conformance to Test.

That is an extremely helpful explanation of what's going on behind the scenes! Thanks! 🍻

In your workaround, you have an assignment to x.a, which gets matched as a narrowing reference later. This is just a general limitation of the design of our control flow narrowing system.

It would be nice for the control flow narrowing system to one day handle this case! 😄

@jtbandes
Copy link
Contributor

I encountered this too: playground link

class C { x?: {a:number} }
class D extends C { x?: {a:number,b:string} }

let c = new C();
// ^?
   c = new D();
// ^?

c.x?.b; // error :(

@ericdrobinson
Copy link
Author

@RyanCavanaugh I see that this has been closed as "completed". Does that mean that an upcoming version of the compiler will support this as expected? If so, any details on what version or otherwise what changes fixed this?

@RyanCavanaugh RyanCavanaugh closed this as not planned Won't fix, can't repro, duplicate, stale Feb 24, 2024
@RyanCavanaugh
Copy link
Member

The bulk "Close" action I applied to Design Limitation issues doesn't let you pick what kind of "Close" you're doing, so don't read too much into any issue's particular bit on that. I wish GitHub didn't make this distinction without a way to specify it in many UI actions!

The correct state for Design Limitation issues is closed since we don't have any plausible way of fixing them, and "Open" issues are for representing "work left to be done".

@Qrokqt
Copy link

Qrokqt commented Mar 14, 2024

This is really frustrating when working with sql query builders. They all seem mostly built on reassignment chaining.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

5 participants