Skip to content

Commit 0aab63b

Browse files
authored
Fix narrowing of optional chains (#36089)
* Check for definitely not undefined instead of maybe not undefined * Fix comment * Add tests
1 parent 3e4578c commit 0aab63b

File tree

6 files changed

+666
-158
lines changed

6 files changed

+666
-158
lines changed

src/compiler/checker.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19777,13 +19777,22 @@ namespace ts {
1977719777
}
1977819778

1977919779
function narrowTypeByOptionalChainContainment(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type {
19780-
// We are in a branch of obj?.foo === value or obj?.foo !== value. We remove undefined and null from
19781-
// the type of obj if (a) the operator is === and the type of value doesn't include undefined or (b) the
19782-
// operator is !== and the type of value is undefined.
19783-
const effectiveTrue = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.EqualsEqualsEqualsToken ? assumeTrue : !assumeTrue;
19784-
const doubleEquals = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken;
19785-
const valueNonNullish = !(getTypeFacts(getTypeOfExpression(value)) & (doubleEquals ? TypeFacts.EQUndefinedOrNull : TypeFacts.EQUndefined));
19786-
return effectiveTrue === valueNonNullish ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type;
19780+
// We are in a branch of obj?.foo === value (or any one of the other equality operators). We narrow obj as follows:
19781+
// When operator is === and type of value excludes undefined, null and undefined is removed from type of obj in true branch.
19782+
// When operator is !== and type of value excludes undefined, null and undefined is removed from type of obj in false branch.
19783+
// When operator is == and type of value excludes null and undefined, null and undefined is removed from type of obj in true branch.
19784+
// When operator is != and type of value excludes null and undefined, null and undefined is removed from type of obj in false branch.
19785+
// When operator is === and type of value is undefined, null and undefined is removed from type of obj in false branch.
19786+
// When operator is !== and type of value is undefined, null and undefined is removed from type of obj in true branch.
19787+
// When operator is == and type of value is null or undefined, null and undefined is removed from type of obj in false branch.
19788+
// When operator is != and type of value is null or undefined, null and undefined is removed from type of obj in true branch.
19789+
const equalsOperator = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.EqualsEqualsEqualsToken;
19790+
const nullableFlags = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken ? TypeFlags.Nullable : TypeFlags.Undefined;
19791+
const valueType = getTypeOfExpression(value);
19792+
// Note that we include any and unknown in the exclusion test because their domain includes null and undefined.
19793+
const removeNullable = equalsOperator !== assumeTrue && everyType(valueType, t => !!(t.flags & nullableFlags)) ||
19794+
equalsOperator === assumeTrue && everyType(valueType, t => !(t.flags & (TypeFlags.AnyOrUnknown | nullableFlags)));
19795+
return removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type;
1978719796
}
1978819797

1978919798
function narrowTypeByEquality(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type {

tests/baselines/reference/controlFlowOptionalChain.errors.txt

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,33 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(310,9): error TS
3636
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(319,9): error TS2532: Object is possibly 'undefined'.
3737
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(322,9): error TS2532: Object is possibly 'undefined'.
3838
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(331,9): error TS2532: Object is possibly 'undefined'.
39+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(334,9): error TS2532: Object is possibly 'undefined'.
40+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(337,9): error TS2532: Object is possibly 'undefined'.
3941
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(340,9): error TS2532: Object is possibly 'undefined'.
4042
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(343,9): error TS2532: Object is possibly 'undefined'.
43+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(346,9): error TS2532: Object is possibly 'undefined'.
44+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(349,9): error TS2532: Object is possibly 'undefined'.
4145
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(352,9): error TS2532: Object is possibly 'undefined'.
42-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(391,9): error TS2532: Object is possibly 'undefined'.
43-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(394,9): error TS2532: Object is possibly 'undefined'.
44-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(403,9): error TS2532: Object is possibly 'undefined'.
45-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(406,9): error TS2532: Object is possibly 'undefined'.
46-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(415,9): error TS2532: Object is possibly 'undefined'.
47-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(424,9): error TS2532: Object is possibly 'undefined'.
48-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(427,9): error TS2532: Object is possibly 'undefined'.
49-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(436,9): error TS2532: Object is possibly 'undefined'.
50-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(471,13): error TS2532: Object is possibly 'undefined'.
51-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(474,13): error TS2532: Object is possibly 'undefined'.
52-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(488,13): error TS2532: Object is possibly 'undefined'.
53-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(491,13): error TS2532: Object is possibly 'undefined'.
46+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(358,9): error TS2532: Object is possibly 'undefined'.
47+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(367,9): error TS2532: Object is possibly 'undefined'.
48+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(370,9): error TS2532: Object is possibly 'undefined'.
49+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(379,9): error TS2532: Object is possibly 'undefined'.
50+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(418,9): error TS2532: Object is possibly 'undefined'.
51+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(421,9): error TS2532: Object is possibly 'undefined'.
52+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(430,9): error TS2532: Object is possibly 'undefined'.
53+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(433,9): error TS2532: Object is possibly 'undefined'.
54+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(442,9): error TS2532: Object is possibly 'undefined'.
55+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(451,9): error TS2532: Object is possibly 'undefined'.
56+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(454,9): error TS2532: Object is possibly 'undefined'.
57+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(463,9): error TS2532: Object is possibly 'undefined'.
58+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(498,13): error TS2532: Object is possibly 'undefined'.
59+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(501,13): error TS2532: Object is possibly 'undefined'.
60+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(515,13): error TS2532: Object is possibly 'undefined'.
61+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(518,13): error TS2532: Object is possibly 'undefined'.
62+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(567,21): error TS2532: Object is possibly 'undefined'.
5463

5564

56-
==== tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts (53 errors) ====
65+
==== tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts (62 errors) ====
5766
// assignments in shortcutting chain
5867
declare const o: undefined | {
5968
[key: string]: any;
@@ -456,6 +465,49 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(491,13): error T
456465
}
457466
}
458467

468+
function f15a(o: Thing | undefined, value: unknown) {
469+
if (o?.foo === value) {
470+
o.foo; // Error
471+
~
472+
!!! error TS2532: Object is possibly 'undefined'.
473+
}
474+
else {
475+
o.foo; // Error
476+
~
477+
!!! error TS2532: Object is possibly 'undefined'.
478+
}
479+
if (o?.foo !== value) {
480+
o.foo; // Error
481+
~
482+
!!! error TS2532: Object is possibly 'undefined'.
483+
}
484+
else {
485+
o.foo; // Error
486+
~
487+
!!! error TS2532: Object is possibly 'undefined'.
488+
}
489+
if (o?.foo == value) {
490+
o.foo; // Error
491+
~
492+
!!! error TS2532: Object is possibly 'undefined'.
493+
}
494+
else {
495+
o.foo; // Error
496+
~
497+
!!! error TS2532: Object is possibly 'undefined'.
498+
}
499+
if (o?.foo != value) {
500+
o.foo; // Error
501+
~
502+
!!! error TS2532: Object is possibly 'undefined'.
503+
}
504+
else {
505+
o.foo; // Error
506+
~
507+
!!! error TS2532: Object is possibly 'undefined'.
508+
}
509+
}
510+
459511
function f16(o: Thing | undefined) {
460512
if (o?.foo === undefined) {
461513
o.foo; // Error
@@ -687,4 +739,29 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(491,13): error T
687739
}
688740
return f.geometry.coordinates;
689741
}
742+
743+
// Repro from #35842
744+
745+
interface SomeObject {
746+
someProperty: unknown;
747+
}
748+
749+
let lastSomeProperty: unknown | undefined;
750+
751+
function someFunction(someOptionalObject: SomeObject | undefined): void {
752+
if (someOptionalObject?.someProperty !== lastSomeProperty) {
753+
console.log(someOptionalObject);
754+
console.log(someOptionalObject.someProperty); // Error
755+
~~~~~~~~~~~~~~~~~~
756+
!!! error TS2532: Object is possibly 'undefined'.
757+
lastSomeProperty = someOptionalObject?.someProperty;
758+
}
759+
}
760+
761+
const someObject: SomeObject = {
762+
someProperty: 42
763+
};
764+
765+
someFunction(someObject);
766+
someFunction(undefined);
690767

tests/baselines/reference/controlFlowOptionalChain.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,33 @@ function f15(o: Thing | undefined, value: number) {
327327
}
328328
}
329329

330+
function f15a(o: Thing | undefined, value: unknown) {
331+
if (o?.foo === value) {
332+
o.foo; // Error
333+
}
334+
else {
335+
o.foo; // Error
336+
}
337+
if (o?.foo !== value) {
338+
o.foo; // Error
339+
}
340+
else {
341+
o.foo; // Error
342+
}
343+
if (o?.foo == value) {
344+
o.foo; // Error
345+
}
346+
else {
347+
o.foo; // Error
348+
}
349+
if (o?.foo != value) {
350+
o.foo; // Error
351+
}
352+
else {
353+
o.foo; // Error
354+
}
355+
}
356+
330357
function f16(o: Thing | undefined) {
331358
if (o?.foo === undefined) {
332359
o.foo; // Error
@@ -526,6 +553,29 @@ function extractCoordinates(f: Feature): number[] {
526553
}
527554
return f.geometry.coordinates;
528555
}
556+
557+
// Repro from #35842
558+
559+
interface SomeObject {
560+
someProperty: unknown;
561+
}
562+
563+
let lastSomeProperty: unknown | undefined;
564+
565+
function someFunction(someOptionalObject: SomeObject | undefined): void {
566+
if (someOptionalObject?.someProperty !== lastSomeProperty) {
567+
console.log(someOptionalObject);
568+
console.log(someOptionalObject.someProperty); // Error
569+
lastSomeProperty = someOptionalObject?.someProperty;
570+
}
571+
}
572+
573+
const someObject: SomeObject = {
574+
someProperty: 42
575+
};
576+
577+
someFunction(someObject);
578+
someFunction(undefined);
529579

530580

531581
//// [controlFlowOptionalChain.js]
@@ -809,6 +859,32 @@ function f15(o, value) {
809859
o.foo;
810860
}
811861
}
862+
function f15a(o, value) {
863+
if ((o === null || o === void 0 ? void 0 : o.foo) === value) {
864+
o.foo; // Error
865+
}
866+
else {
867+
o.foo; // Error
868+
}
869+
if ((o === null || o === void 0 ? void 0 : o.foo) !== value) {
870+
o.foo; // Error
871+
}
872+
else {
873+
o.foo; // Error
874+
}
875+
if ((o === null || o === void 0 ? void 0 : o.foo) == value) {
876+
o.foo; // Error
877+
}
878+
else {
879+
o.foo; // Error
880+
}
881+
if ((o === null || o === void 0 ? void 0 : o.foo) != value) {
882+
o.foo; // Error
883+
}
884+
else {
885+
o.foo; // Error
886+
}
887+
}
812888
function f16(o) {
813889
if ((o === null || o === void 0 ? void 0 : o.foo) === undefined) {
814890
o.foo; // Error
@@ -982,3 +1058,16 @@ function extractCoordinates(f) {
9821058
}
9831059
return f.geometry.coordinates;
9841060
}
1061+
var lastSomeProperty;
1062+
function someFunction(someOptionalObject) {
1063+
if ((someOptionalObject === null || someOptionalObject === void 0 ? void 0 : someOptionalObject.someProperty) !== lastSomeProperty) {
1064+
console.log(someOptionalObject);
1065+
console.log(someOptionalObject.someProperty); // Error
1066+
lastSomeProperty = someOptionalObject === null || someOptionalObject === void 0 ? void 0 : someOptionalObject.someProperty;
1067+
}
1068+
}
1069+
var someObject = {
1070+
someProperty: 42
1071+
};
1072+
someFunction(someObject);
1073+
someFunction(undefined);

0 commit comments

Comments
 (0)