Skip to content

Commit afe6f4c

Browse files
authored
Merge pull request #33821 from microsoft/fix33806
Reflect control flow effects of optional chains in equality checks
2 parents d552675 + 99cec3a commit afe6f4c

File tree

7 files changed

+1957
-28
lines changed

7 files changed

+1957
-28
lines changed

src/compiler/binder.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -856,9 +856,8 @@ namespace ts {
856856
function isNarrowableReference(expr: Expression): boolean {
857857
return expr.kind === SyntaxKind.Identifier || expr.kind === SyntaxKind.ThisKeyword || expr.kind === SyntaxKind.SuperKeyword ||
858858
(isPropertyAccessExpression(expr) || isNonNullExpression(expr) || isParenthesizedExpression(expr)) && isNarrowableReference(expr.expression) ||
859-
isElementAccessExpression(expr) &&
860-
isStringOrNumericLiteralLike(expr.argumentExpression) &&
861-
isNarrowableReference(expr.expression);
859+
isElementAccessExpression(expr) && isStringOrNumericLiteralLike(expr.argumentExpression) && isNarrowableReference(expr.expression) ||
860+
isOptionalChain(expr);
862861
}
863862

864863
function hasNarrowableArgument(expr: CallExpression) {

src/compiler/checker.ts

+37
Original file line numberDiff line numberDiff line change
@@ -18124,6 +18124,16 @@ namespace ts {
1812418124
return false;
1812518125
}
1812618126

18127+
function optionalChainContainsReference(source: Node, target: Node) {
18128+
while (isOptionalChain(source)) {
18129+
source = source.expression;
18130+
if (isMatchingReference(source, target)) {
18131+
return true;
18132+
}
18133+
}
18134+
return false;
18135+
}
18136+
1812718137
// Return true if target is a property access xxx.yyy, source is a property access xxx.zzz, the declared
1812818138
// type of xxx is a union type, and yyy is a property that is possibly a discriminant. We consider a property
1812918139
// a possible discriminant if its type differs in the constituents of containing union type, and if every
@@ -19350,6 +19360,14 @@ namespace ts {
1935019360
if (isMatchingReference(reference, right)) {
1935119361
return narrowTypeByEquality(type, operator, left, assumeTrue);
1935219362
}
19363+
if (assumeTrue && strictNullChecks) {
19364+
if (optionalChainContainsReference(left, reference)) {
19365+
type = narrowTypeByOptionalChainContainment(type, operator, right);
19366+
}
19367+
else if (optionalChainContainsReference(right, reference)) {
19368+
type = narrowTypeByOptionalChainContainment(type, operator, left);
19369+
}
19370+
}
1935319371
if (isMatchingReferenceDiscriminant(left, declaredType)) {
1935419372
return narrowTypeByDiscriminant(type, <AccessExpression>left, t => narrowTypeByEquality(t, operator, right, assumeTrue));
1935519373
}
@@ -19374,6 +19392,18 @@ namespace ts {
1937419392
return type;
1937519393
}
1937619394

19395+
function narrowTypeByOptionalChainContainment(type: Type, operator: SyntaxKind, value: Expression): Type {
19396+
// We are in the true branch of obj?.foo === value or obj?.foo !== value. We remove undefined and null from
19397+
// the type of obj if (a) the operator is === and the type of value doesn't include undefined or (b) the
19398+
// operator is !== and the type of value is undefined.
19399+
const valueType = getTypeOfExpression(value);
19400+
return operator === SyntaxKind.EqualsEqualsToken && !(getTypeFacts(valueType) & TypeFacts.EQUndefinedOrNull) ||
19401+
operator === SyntaxKind.EqualsEqualsEqualsToken && !(getTypeFacts(valueType) & TypeFacts.EQUndefined) ||
19402+
operator === SyntaxKind.ExclamationEqualsToken && valueType.flags & TypeFlags.Nullable ||
19403+
operator === SyntaxKind.ExclamationEqualsEqualsToken && valueType.flags & TypeFlags.Undefined ?
19404+
getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type;
19405+
}
19406+
1937719407
function narrowTypeByEquality(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type {
1937819408
if (type.flags & TypeFlags.Any) {
1937919409
return type;
@@ -19424,6 +19454,10 @@ namespace ts {
1942419454
// We have '==', '!=', '===', or !==' operator with 'typeof xxx' and string literal operands
1942519455
const target = getReferenceCandidate(typeOfExpr.expression);
1942619456
if (!isMatchingReference(reference, target)) {
19457+
if (assumeTrue && (operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.EqualsEqualsEqualsToken) &&
19458+
strictNullChecks && optionalChainContainsReference(target, reference)) {
19459+
return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
19460+
}
1942719461
// For a reference of the form 'x.y', a 'typeof x === ...' type guard resets the
1942819462
// narrowed type of 'y' to its declared type.
1942919463
if (containsMatchingReference(reference, target)) {
@@ -19605,6 +19639,9 @@ namespace ts {
1960519639
function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
1960619640
const left = getReferenceCandidate(expr.left);
1960719641
if (!isMatchingReference(reference, left)) {
19642+
if (assumeTrue && strictNullChecks && optionalChainContainsReference(left, reference)) {
19643+
return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
19644+
}
1960819645
// For a reference of the form 'x.y', an 'x instanceof T' type guard resets the
1960919646
// narrowed type of 'y' to its declared type. We do this because preceding 'x.y'
1961019647
// references might reference a different 'y' property. However, we make an exception

tests/baselines/reference/controlFlowOptionalChain.errors.txt

+209-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(35,5): error TS2
66
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(39,1): error TS2722: Cannot invoke an object which is possibly 'undefined'.
77
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(52,5): error TS2532: Object is possibly 'undefined'.
88
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(57,1): error TS2532: Object is possibly 'undefined'.
9-
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(62,5): error TS2532: Object is possibly 'undefined'.
109
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(68,5): error TS2532: Object is possibly 'undefined'.
1110
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(72,1): error TS2532: Object is possibly 'undefined'.
1211
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(83,5): error TS2532: Object is possibly 'undefined'.
@@ -20,9 +19,21 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(112,1): error TS
2019
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(130,5): error TS2532: Object is possibly 'undefined'.
2120
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(134,1): error TS2532: Object is possibly 'undefined'.
2221
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(153,9): error TS2775: Assertions require every name in the call target to be declared with an explicit type annotation.
22+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(208,9): error TS2532: Object is possibly 'undefined'.
23+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(211,9): error TS2532: Object is possibly 'undefined'.
24+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(214,9): error TS2532: Object is possibly 'undefined'.
25+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(217,9): error TS2532: Object is possibly 'undefined'.
26+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(220,9): error TS2532: Object is possibly 'undefined'.
27+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(223,9): error TS2532: Object is possibly 'undefined'.
28+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(238,9): error TS2532: Object is possibly 'undefined'.
29+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(241,9): error TS2532: Object is possibly 'undefined'.
30+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(244,9): error TS2532: Object is possibly 'undefined'.
31+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(271,9): error TS2532: Object is possibly 'undefined'.
32+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(274,9): error TS2532: Object is possibly 'undefined'.
33+
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(277,9): error TS2532: Object is possibly 'undefined'.
2334

2435

25-
==== tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts (22 errors) ====
36+
==== tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts (33 errors) ====
2637
// assignments in shortcutting chain
2738
declare const o: undefined | {
2839
[key: string]: any;
@@ -99,10 +110,8 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(153,9): error TS
99110

100111
declare const o3: { x: 1, y: string } | { x: 2, y: number } | undefined;
101112
if (o3?.x === 1) {
102-
o3; // TODO: should be `{ x: y, y: string }`
103-
o3.x; // TODO: should not be an error.
104-
~~
105-
!!! error TS2532: Object is possibly 'undefined'.
113+
o3;
114+
o3.x;
106115
o3?.x;
107116
}
108117
else {
@@ -227,4 +236,198 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(153,9): error TS
227236
x;
228237
}
229238
}
239+
240+
type Thing = { foo: string | number, bar(): number, baz: object };
241+
242+
function f10(o: Thing | undefined, value: number) {
243+
if (o?.foo === value) {
244+
o.foo;
245+
}
246+
if (o?.["foo"] === value) {
247+
o["foo"];
248+
}
249+
if (o?.bar() === value) {
250+
o.bar;
251+
}
252+
if (o?.foo == value) {
253+
o.foo;
254+
}
255+
if (o?.["foo"] == value) {
256+
o["foo"];
257+
}
258+
if (o?.bar() == value) {
259+
o.bar;
260+
}
261+
}
262+
263+
function f11(o: Thing | null, value: number) {
264+
if (o?.foo === value) {
265+
o.foo;
266+
}
267+
if (o?.["foo"] === value) {
268+
o["foo"];
269+
}
270+
if (o?.bar() === value) {
271+
o.bar;
272+
}
273+
if (o?.foo == value) {
274+
o.foo;
275+
}
276+
if (o?.["foo"] == value) {
277+
o["foo"];
278+
}
279+
if (o?.bar() == value) {
280+
o.bar;
281+
}
282+
}
283+
284+
function f12(o: Thing | undefined, value: number | undefined) {
285+
if (o?.foo === value) {
286+
o.foo; // Error
287+
~
288+
!!! error TS2532: Object is possibly 'undefined'.
289+
}
290+
if (o?.["foo"] === value) {
291+
o["foo"]; // Error
292+
~
293+
!!! error TS2532: Object is possibly 'undefined'.
294+
}
295+
if (o?.bar() === value) {
296+
o.bar; // Error
297+
~
298+
!!! error TS2532: Object is possibly 'undefined'.
299+
}
300+
if (o?.foo == value) {
301+
o.foo; // Error
302+
~
303+
!!! error TS2532: Object is possibly 'undefined'.
304+
}
305+
if (o?.["foo"] == value) {
306+
o["foo"]; // Error
307+
~
308+
!!! error TS2532: Object is possibly 'undefined'.
309+
}
310+
if (o?.bar() == value) {
311+
o.bar; // Error
312+
~
313+
!!! error TS2532: Object is possibly 'undefined'.
314+
}
315+
}
316+
317+
function f12a(o: Thing | undefined, value: number | null) {
318+
if (o?.foo === value) {
319+
o.foo;
320+
}
321+
if (o?.["foo"] === value) {
322+
o["foo"];
323+
}
324+
if (o?.bar() === value) {
325+
o.bar;
326+
}
327+
if (o?.foo == value) {
328+
o.foo; // Error
329+
~
330+
!!! error TS2532: Object is possibly 'undefined'.
331+
}
332+
if (o?.["foo"] == value) {
333+
o["foo"]; // Error
334+
~
335+
!!! error TS2532: Object is possibly 'undefined'.
336+
}
337+
if (o?.bar() == value) {
338+
o.bar; // Error
339+
~
340+
!!! error TS2532: Object is possibly 'undefined'.
341+
}
342+
}
343+
344+
function f13(o: Thing | undefined) {
345+
if (o?.foo !== undefined) {
346+
o.foo;
347+
}
348+
if (o?.["foo"] !== undefined) {
349+
o["foo"];
350+
}
351+
if (o?.bar() !== undefined) {
352+
o.bar;
353+
}
354+
if (o?.foo != undefined) {
355+
o.foo;
356+
}
357+
if (o?.["foo"] != undefined) {
358+
o["foo"];
359+
}
360+
if (o?.bar() != undefined) {
361+
o.bar;
362+
}
363+
}
364+
365+
function f13a(o: Thing | undefined) {
366+
if (o?.foo !== null) {
367+
o.foo; // Error
368+
~
369+
!!! error TS2532: Object is possibly 'undefined'.
370+
}
371+
if (o?.["foo"] !== null) {
372+
o["foo"]; // Error
373+
~
374+
!!! error TS2532: Object is possibly 'undefined'.
375+
}
376+
if (o?.bar() !== null) {
377+
o.bar; // Error
378+
~
379+
!!! error TS2532: Object is possibly 'undefined'.
380+
}
381+
if (o?.foo != null) {
382+
o.foo;
383+
}
384+
if (o?.["foo"] != null) {
385+
o["foo"];
386+
}
387+
if (o?.bar() != null) {
388+
o.bar;
389+
}
390+
}
391+
392+
function f14(o: Thing | null) {
393+
if (o?.foo !== undefined) {
394+
o.foo;
395+
}
396+
if (o?.["foo"] !== undefined) {
397+
o["foo"];
398+
}
399+
if (o?.bar() !== undefined) {
400+
o.bar;
401+
}
402+
}
403+
404+
function f20(o: Thing | undefined) {
405+
if (typeof o?.foo === "number") {
406+
o.foo;
407+
}
408+
if (typeof o?.["foo"] === "number") {
409+
o["foo"];
410+
}
411+
if (typeof o?.bar() === "number") {
412+
o.bar;
413+
}
414+
if (o?.baz instanceof Error) {
415+
o.baz;
416+
}
417+
}
418+
419+
function f21(o: Thing | null) {
420+
if (typeof o?.foo === "number") {
421+
o.foo;
422+
}
423+
if (typeof o?.["foo"] === "number") {
424+
o["foo"];
425+
}
426+
if (typeof o?.bar() === "number") {
427+
o.bar;
428+
}
429+
if (o?.baz instanceof Error) {
430+
o.baz;
431+
}
432+
}
230433

0 commit comments

Comments
 (0)