Skip to content

exactOptionalPropertyTypes does not include undefined in the type for assignment operators like ??= #53305

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
bradzacher opened this issue Mar 17, 2023 · 5 comments Β· Fixed by #54777
Labels
Bug A bug in TypeScript Help Wanted You can do this
Milestone

Comments

@bradzacher
Copy link
Contributor

bradzacher commented Mar 17, 2023

Bug Report

πŸ”Ž Search Terms

assignment operator, noUncheckedIndexedAccess

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

// @exactOptionalPropertyTypes

declare const xx: { prop?: number };

xx.prop;
//  ^?
// typeof xx.prop === number | undefined βœ…

function aa() {
xx.prop.toFixed();
//  ^?
// typeof xx.prop === number | undefined βœ…
}

function bb() {
xx.prop += 1;
//  ^?
// typeof xx.prop === number | undefined βœ…
}

function cc() {
xx.prop ??= 1;
//  ^?
// typeof xx.prop === number ❌
}

function dd() {
xx.prop ?? (xx.prop = 1);
//  ^?
// typeof xx.prop === number | undefined βœ…
}

function ee() {
xx.prop = 1;
//  ^?
// typeof xx.prop === number ❌
}

πŸ™ Actual behavior

TS reports the type of xx.prop to be just number when using the ??= assignment operator.
This is incorrect - xx.prop might be undefined until AFTER the assignment and also is different behaviour compared to other assignment operators and different to how the expanded assignment is typed.

πŸ™‚ Expected behavior

TS reports the type of xx.prop to be number | undefined before the assignment.

Additional info

In addition to the above - this behaviour makes it difficult for us to write lint rules because when we attempt to check if the LHS of the assignment is undefined, the type system reports that it is not. Example issue typescript-eslint/typescript-eslint#6635

@RyanCavanaugh
Copy link
Member

Same as the other one

@chharvey
Copy link

chharvey commented Mar 17, 2023

@RyanCavanaugh I think this is different from #53306; this one is more about consistency. It appears the left-hand side of an augmentation operator (+=, ??=, etc) is showing a different type based on what operator is used. When you inspect the type via intellisense, sometimes it reports the type β€œbefore” the assignment, and sometimes β€œafter”.

In the examples above, for xx.prop += 1, why is xx.prop showing number | undefined, whereas in xx.prop = 1 it’s showing just number? In both expressions, the type of xx.prop is number | undefined before the assignment and is number afterward. Shouldn’t both situations be handled the same?

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript and removed Working as Intended The behavior described is the intended behavior; this is not a bug labels Mar 17, 2023
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 17, 2023

I guess I disagree with the OP about where the problem in this sample is.

In all cases where an expression is an assignment target, the intended behavior is to show the write type, which is actually neither the pre-assignment CFA type nor the post-assignment CFA type. So in

xx.prop += 1;
// xx.prop: number | undefined

This is marked in the repro as βœ…, but the bug is that this is the wrong one, it should be number (under EOPT only) since that's the write type of xx.prop. This line is ❌.

Conversely, this one

xx.prop ??= 1;
//  ^?
// typeof xx.prop === number ❌

is actually the intended behavior, it's βœ…

@RyanCavanaugh RyanCavanaugh added the Help Wanted You can do this label Mar 17, 2023
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Mar 17, 2023
@chharvey
Copy link

Agreed, under EOPT it should be number only, to prevent things like xx.prop = undefined. @bradzacher, looks like typescript-eslint/typescript-eslint#6635 will have to find an independent solution

@bradzacher
Copy link
Contributor Author

The thing I'm looking at is whether nor not TS can have a read and a write type?
For example for noUncheckedIndexAccess if you write to a property then undefined is disallowed, but if you read from it then it could be undefined.

But based on what Ryan has said - it sounds like the type of the left expression that we are seeing is the write type - hence it isn't undefined.

Originally I had assumed the read/write differentiation was done as part of the assignment checking logic - but from what I'm reading here that's incorrect, and instead TS resolves the type to be the write type and then the assignment logic doesn't need to worry about the difference.

Which is fair, but does suck for us as we don't have the infra to be able to understand that we could have an undefined in other cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Help Wanted You can do this
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants