Skip to content

Commit e231821

Browse files
authored
Strip nullable types from 'this' type in inference with optional chain calls (microsoft#42536)
* Properly strip nullable types from this type in optional chain calls * Add regression test
1 parent cce4bfb commit e231821

File tree

5 files changed

+130
-16
lines changed

5 files changed

+130
-16
lines changed

src/compiler/checker.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27160,6 +27160,16 @@ namespace ts {
2716027160
return getInferredTypes(context);
2716127161
}
2716227162

27163+
function getThisArgumentType(thisArgumentNode: LeftHandSideExpression | undefined) {
27164+
if (!thisArgumentNode) {
27165+
return voidType;
27166+
}
27167+
const thisArgumentType = checkExpression(thisArgumentNode);
27168+
return isOptionalChainRoot(thisArgumentNode.parent) ? getNonNullableType(thisArgumentType) :
27169+
isOptionalChain(thisArgumentNode.parent) ? removeOptionalTypeMarker(thisArgumentType) :
27170+
thisArgumentType;
27171+
}
27172+
2716327173
function inferTypeArguments(node: CallLikeExpression, signature: Signature, args: readonly Expression[], checkMode: CheckMode, context: InferenceContext): Type[] {
2716427174
if (isJsxOpeningLikeElement(node)) {
2716527175
return inferJsxTypeArguments(node, signature, checkMode, context);
@@ -27215,8 +27225,7 @@ namespace ts {
2721527225
const thisType = getThisTypeOfSignature(signature);
2721627226
if (thisType) {
2721727227
const thisArgumentNode = getThisArgumentOfCall(node);
27218-
const thisArgumentType = thisArgumentNode ? checkExpression(thisArgumentNode) : voidType;
27219-
inferTypes(context.inferences, thisArgumentType, thisType);
27228+
inferTypes(context.inferences, getThisArgumentType(thisArgumentNode), thisType);
2722027229
}
2722127230

2722227231
for (let i = 0; i < argCount; i++) {
@@ -27457,20 +27466,7 @@ namespace ts {
2745727466
// If the signature's 'this' type is voidType, then the check is skipped -- anything is compatible.
2745827467
// If the expression is a new expression, then the check is skipped.
2745927468
const thisArgumentNode = getThisArgumentOfCall(node);
27460-
let thisArgumentType: Type;
27461-
if (thisArgumentNode) {
27462-
thisArgumentType = checkExpression(thisArgumentNode);
27463-
if (isOptionalChainRoot(thisArgumentNode.parent)) {
27464-
thisArgumentType = getNonNullableType(thisArgumentType);
27465-
}
27466-
else if (isOptionalChain(thisArgumentNode.parent)) {
27467-
thisArgumentType = removeOptionalTypeMarker(thisArgumentType);
27468-
}
27469-
}
27470-
else {
27471-
thisArgumentType = voidType;
27472-
}
27473-
27469+
const thisArgumentType = getThisArgumentType(thisArgumentNode);
2747427470
const errorNode = reportErrors ? (thisArgumentNode || node) : undefined;
2747527471
const headMessage = Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1;
2747627472
if (!checkTypeRelatedTo(thisArgumentType, thisType, relation, errorNode, headMessage, containingMessageChain, errorOutputContainer)) {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//// [callChainInference.ts]
2+
// Repro from #42404
3+
4+
interface Y {
5+
foo<T>(this: T, arg: keyof T): void;
6+
a: number;
7+
b: string;
8+
}
9+
10+
declare const value: Y | undefined;
11+
12+
if (value) {
13+
value?.foo("a");
14+
}
15+
16+
value?.foo("a");
17+
18+
19+
//// [callChainInference.js]
20+
"use strict";
21+
// Repro from #42404
22+
if (value) {
23+
value === null || value === void 0 ? void 0 : value.foo("a");
24+
}
25+
value === null || value === void 0 ? void 0 : value.foo("a");
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
=== tests/cases/conformance/expressions/optionalChaining/callChain/callChainInference.ts ===
2+
// Repro from #42404
3+
4+
interface Y {
5+
>Y : Symbol(Y, Decl(callChainInference.ts, 0, 0))
6+
7+
foo<T>(this: T, arg: keyof T): void;
8+
>foo : Symbol(Y.foo, Decl(callChainInference.ts, 2, 13))
9+
>T : Symbol(T, Decl(callChainInference.ts, 3, 8))
10+
>this : Symbol(this, Decl(callChainInference.ts, 3, 11))
11+
>T : Symbol(T, Decl(callChainInference.ts, 3, 8))
12+
>arg : Symbol(arg, Decl(callChainInference.ts, 3, 19))
13+
>T : Symbol(T, Decl(callChainInference.ts, 3, 8))
14+
15+
a: number;
16+
>a : Symbol(Y.a, Decl(callChainInference.ts, 3, 40))
17+
18+
b: string;
19+
>b : Symbol(Y.b, Decl(callChainInference.ts, 4, 14))
20+
}
21+
22+
declare const value: Y | undefined;
23+
>value : Symbol(value, Decl(callChainInference.ts, 8, 13))
24+
>Y : Symbol(Y, Decl(callChainInference.ts, 0, 0))
25+
26+
if (value) {
27+
>value : Symbol(value, Decl(callChainInference.ts, 8, 13))
28+
29+
value?.foo("a");
30+
>value?.foo : Symbol(Y.foo, Decl(callChainInference.ts, 2, 13))
31+
>value : Symbol(value, Decl(callChainInference.ts, 8, 13))
32+
>foo : Symbol(Y.foo, Decl(callChainInference.ts, 2, 13))
33+
}
34+
35+
value?.foo("a");
36+
>value?.foo : Symbol(Y.foo, Decl(callChainInference.ts, 2, 13))
37+
>value : Symbol(value, Decl(callChainInference.ts, 8, 13))
38+
>foo : Symbol(Y.foo, Decl(callChainInference.ts, 2, 13))
39+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
=== tests/cases/conformance/expressions/optionalChaining/callChain/callChainInference.ts ===
2+
// Repro from #42404
3+
4+
interface Y {
5+
foo<T>(this: T, arg: keyof T): void;
6+
>foo : <T>(this: T, arg: keyof T) => void
7+
>this : T
8+
>arg : keyof T
9+
10+
a: number;
11+
>a : number
12+
13+
b: string;
14+
>b : string
15+
}
16+
17+
declare const value: Y | undefined;
18+
>value : Y | undefined
19+
20+
if (value) {
21+
>value : Y | undefined
22+
23+
value?.foo("a");
24+
>value?.foo("a") : void
25+
>value?.foo : <T>(this: T, arg: keyof T) => void
26+
>value : Y
27+
>foo : <T>(this: T, arg: keyof T) => void
28+
>"a" : "a"
29+
}
30+
31+
value?.foo("a");
32+
>value?.foo("a") : void | undefined
33+
>value?.foo : (<T>(this: T, arg: keyof T) => void) | undefined
34+
>value : Y | undefined
35+
>foo : (<T>(this: T, arg: keyof T) => void) | undefined
36+
>"a" : "a"
37+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// @strict: true
2+
3+
// Repro from #42404
4+
5+
interface Y {
6+
foo<T>(this: T, arg: keyof T): void;
7+
a: number;
8+
b: string;
9+
}
10+
11+
declare const value: Y | undefined;
12+
13+
if (value) {
14+
value?.foo("a");
15+
}
16+
17+
value?.foo("a");

0 commit comments

Comments
 (0)