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 all commits
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
49 changes: 49 additions & 0 deletions tests/baselines/reference/controlFlowOptionalChain3.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
controlFlowOptionalChain3.tsx(30,8): error TS18048: 'foo' is possibly 'undefined'.
controlFlowOptionalChain3.tsx(36,31): error TS18048: 'options' is possibly 'undefined'.


==== controlFlowOptionalChain3.tsx (2 errors) ====
/// <reference path="/.lib/react16.d.ts" />

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

import React from "react";

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;
}
}

function Test3({ foo }: { foo: Foo | undefined }) {
return (
<div>
{foo?.bar === false && "foo"}
{foo.bar ? "true" : "false"}
~~~
!!! error TS18048: 'foo' is possibly 'undefined'.
</div>
);
}

function test4(options?: { a?: boolean; b?: boolean }) {
if (options?.a === false || options.b) {
~~~~~~~
!!! error TS18048: 'options' is possibly 'undefined'.
options;
}
}

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

=== controlFlowOptionalChain3.tsx ===
/// <reference path="react16.d.ts" />

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

import React from "react";
>React : Symbol(React, Decl(controlFlowOptionalChain3.tsx, 4, 6))

interface Foo {
>Foo : Symbol(Foo, Decl(controlFlowOptionalChain3.tsx, 4, 26))

bar: boolean;
>bar : Symbol(Foo.bar, Decl(controlFlowOptionalChain3.tsx, 6, 15))
}

function test1(foo: Foo | undefined) {
>test1 : Symbol(test1, Decl(controlFlowOptionalChain3.tsx, 8, 1))
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.tsx, 10, 15))
>Foo : Symbol(Foo, Decl(controlFlowOptionalChain3.tsx, 4, 26))

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

foo;
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.tsx, 10, 15))
}
foo;
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.tsx, 10, 15))
}

function test2(foo: Foo | undefined) {
>test2 : Symbol(test2, Decl(controlFlowOptionalChain3.tsx, 15, 1))
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.tsx, 17, 15))
>Foo : Symbol(Foo, Decl(controlFlowOptionalChain3.tsx, 4, 26))

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

foo;
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.tsx, 17, 15))

} else {
foo;
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.tsx, 17, 15))
}
}

function Test3({ foo }: { foo: Foo | undefined }) {
>Test3 : Symbol(Test3, Decl(controlFlowOptionalChain3.tsx, 23, 1))
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.tsx, 25, 16))
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.tsx, 25, 25))
>Foo : Symbol(Foo, Decl(controlFlowOptionalChain3.tsx, 4, 26))

return (
<div>
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))

{foo?.bar === false && "foo"}
>foo?.bar : Symbol(Foo.bar, Decl(controlFlowOptionalChain3.tsx, 6, 15))
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.tsx, 25, 16))
>bar : Symbol(Foo.bar, Decl(controlFlowOptionalChain3.tsx, 6, 15))

{foo.bar ? "true" : "false"}
>foo.bar : Symbol(Foo.bar, Decl(controlFlowOptionalChain3.tsx, 6, 15))
>foo : Symbol(foo, Decl(controlFlowOptionalChain3.tsx, 25, 16))
>bar : Symbol(Foo.bar, Decl(controlFlowOptionalChain3.tsx, 6, 15))

</div>
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))

);
}

function test4(options?: { a?: boolean; b?: boolean }) {
>test4 : Symbol(test4, Decl(controlFlowOptionalChain3.tsx, 32, 1))
>options : Symbol(options, Decl(controlFlowOptionalChain3.tsx, 34, 15))
>a : Symbol(a, Decl(controlFlowOptionalChain3.tsx, 34, 26))
>b : Symbol(b, Decl(controlFlowOptionalChain3.tsx, 34, 39))

if (options?.a === false || options.b) {
>options?.a : Symbol(a, Decl(controlFlowOptionalChain3.tsx, 34, 26))
>options : Symbol(options, Decl(controlFlowOptionalChain3.tsx, 34, 15))
>a : Symbol(a, Decl(controlFlowOptionalChain3.tsx, 34, 26))
>options.b : Symbol(b, Decl(controlFlowOptionalChain3.tsx, 34, 39))
>options : Symbol(options, Decl(controlFlowOptionalChain3.tsx, 34, 15))
>b : Symbol(b, Decl(controlFlowOptionalChain3.tsx, 34, 39))

options;
>options : Symbol(options, Decl(controlFlowOptionalChain3.tsx, 34, 15))
}
}

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

=== controlFlowOptionalChain3.tsx ===
/// <reference path="react16.d.ts" />

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

import React from "react";
>React : typeof React

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
}
}

function Test3({ foo }: { foo: Foo | undefined }) {
>Test3 : ({ foo }: { foo: Foo | undefined; }) => JSX.Element
>foo : Foo | undefined
>foo : Foo | undefined

return (
>( <div> {foo?.bar === false && "foo"} {foo.bar ? "true" : "false"} </div> ) : JSX.Element

<div>
><div> {foo?.bar === false && "foo"} {foo.bar ? "true" : "false"} </div> : JSX.Element
>div : any

{foo?.bar === false && "foo"}
>foo?.bar === false && "foo" : false | "foo"
>foo?.bar === false : boolean
>foo?.bar : boolean | undefined
>foo : Foo | undefined
>bar : boolean | undefined
>false : false
>"foo" : "foo"

{foo.bar ? "true" : "false"}
>foo.bar ? "true" : "false" : "false" | "true"
>foo.bar : boolean
>foo : Foo | undefined
>bar : boolean
>"true" : "true"
>"false" : "false"

</div>
>div : any

);
}

function test4(options?: { a?: boolean; b?: boolean }) {
>test4 : (options?: { a?: boolean; b?: boolean;}) => void
>options : { a?: boolean | undefined; b?: boolean | undefined; } | undefined
>a : boolean | undefined
>b : boolean | undefined

if (options?.a === false || options.b) {
>options?.a === false || options.b : boolean | undefined
>options?.a === false : boolean
>options?.a : boolean | undefined
>options : { a?: boolean | undefined; b?: boolean | undefined; } | undefined
>a : boolean | undefined
>false : false
>options.b : boolean | undefined
>options : { a?: boolean | undefined; b?: boolean | undefined; } | undefined
>b : boolean | undefined

options;
>options : { a?: boolean | undefined; b?: boolean | undefined; } | undefined
}
}

44 changes: 44 additions & 0 deletions tests/cases/conformance/controlFlow/controlFlowOptionalChain3.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// @strict: true
// @noEmit: true
// @esModuleInterop: true
// @jsx: react

/// <reference path="/.lib/react16.d.ts" />

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

import React from "react";

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;
}
}

function Test3({ foo }: { foo: Foo | undefined }) {
return (
<div>
{foo?.bar === false && "foo"}
{foo.bar ? "true" : "false"}
</div>
);
}

function test4(options?: { a?: boolean; b?: boolean }) {
if (options?.a === false || options.b) {
options;
}
}