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

Fixed an issue in boolean comparison narrowing when the reference is an optional chain #56504

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27929,10 +27929,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (isMatchingConstructorReference(right)) {
return narrowTypeByConstructor(type, operator, left, assumeTrue);
}
if (isBooleanLiteral(right)) {
if (isBooleanLiteral(right) && !isAccessExpression(left)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that the original type can be reassigned based on optionalChainContainsReference. That messed up the else branch because narrowTypeByBooleanComparison was operating on the wrong type.

I've also tried keeping originalType around and pass that to narrowTypeByBooleanComparison. That fixed the reported issue~ related to the wrong type in the else branch but it broke the same type within the if block. This happened because originally the intention was for that reassigned type to be returned through the return at the bottom of this function but I changed it to return originalType through narrowTypeByBooleanComparison.

So all in all, this seems like the simplest fix and reflects the intention the best. The motivation behind the original change was to improve non-access expression narrowing. Access expressions are already handled by discrimination-based narrowing etc.

Even though it worked previously (as far as I know) for non-optional chain access expressions, this helps them avoid some redundant work too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems fine as a quick fix for now, though I suspect that we could try and do better later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then again, not sure what "better" even means; an expression like (options?.a === false || options.b is not the same as (!options?.a || options.b) so realistically I don't think there's any extra info gained here.

return narrowTypeByBooleanComparison(type, left, right, operator, assumeTrue);
}
if (isBooleanLiteral(left)) {
if (isBooleanLiteral(left) && !isAccessExpression(right)) {
return narrowTypeByBooleanComparison(type, right, left, operator, assumeTrue);
}
break;
Expand Down
48 changes: 48 additions & 0 deletions tests/baselines/reference/controlFlowOptionalChain3.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//// [tests/cases/conformance/controlFlow/controlFlowOptionalChain3.ts] ////

=== controlFlowOptionalChain3.ts ===
// https://github.com/microsoft/TypeScript/issues/56482

interface Foo {
>Foo : Symbol(Foo, Decl(controlFlowOptionalChain3.ts, 0, 0))

bar: boolean;
>bar : Symbol(Foo.bar, Decl(controlFlowOptionalChain3.ts, 2, 15))
}

function test1(foo: Foo | undefined) {
>test1 : Symbol(test1, Decl(controlFlowOptionalChain3.ts, 4, 1))
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.ts, 6, 15))
>Foo : Symbol(Foo, Decl(controlFlowOptionalChain3.ts, 0, 0))

if (foo?.bar === false) {
>foo?.bar : Symbol(Foo.bar, Decl(controlFlowOptionalChain3.ts, 2, 15))
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.ts, 6, 15))
>bar : Symbol(Foo.bar, Decl(controlFlowOptionalChain3.ts, 2, 15))

foo;
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.ts, 6, 15))
}
foo;
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.ts, 6, 15))
}

function test2(foo: Foo | undefined) {
>test2 : Symbol(test2, Decl(controlFlowOptionalChain3.ts, 11, 1))
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.ts, 13, 15))
>Foo : Symbol(Foo, Decl(controlFlowOptionalChain3.ts, 0, 0))

if (foo?.bar === false) {
>foo?.bar : Symbol(Foo.bar, Decl(controlFlowOptionalChain3.ts, 2, 15))
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.ts, 13, 15))
>bar : Symbol(Foo.bar, Decl(controlFlowOptionalChain3.ts, 2, 15))

foo;
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.ts, 13, 15))

} else {
foo;
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.ts, 13, 15))
}
}

48 changes: 48 additions & 0 deletions tests/baselines/reference/controlFlowOptionalChain3.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//// [tests/cases/conformance/controlFlow/controlFlowOptionalChain3.ts] ////

=== controlFlowOptionalChain3.ts ===
// https://github.com/microsoft/TypeScript/issues/56482

interface Foo {
bar: boolean;
>bar : boolean
}

function test1(foo: Foo | undefined) {
>test1 : (foo: Foo | undefined) => void
>foo : Foo | undefined

if (foo?.bar === false) {
>foo?.bar === false : boolean
>foo?.bar : boolean | undefined
>foo : Foo | undefined
>bar : boolean | undefined
>false : false

foo;
>foo : Foo
}
foo;
>foo : Foo | undefined
}

function test2(foo: Foo | undefined) {
>test2 : (foo: Foo | undefined) => void
>foo : Foo | undefined

if (foo?.bar === false) {
>foo?.bar === false : boolean
>foo?.bar : boolean | undefined
>foo : Foo | undefined
>bar : boolean | undefined
>false : false

foo;
>foo : Foo

} else {
foo;
>foo : Foo | undefined
}
}

23 changes: 23 additions & 0 deletions tests/cases/conformance/controlFlow/controlFlowOptionalChain3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @strict: true
// @noEmit: true

// https://github.com/microsoft/TypeScript/issues/56482

interface Foo {
bar: boolean;
}

function test1(foo: Foo | undefined) {
if (foo?.bar === false) {
foo;
}
foo;
}

function test2(foo: Foo | undefined) {
if (foo?.bar === false) {
foo;
} else {
foo;
}
}