From 4e514e3e7325c4eb3760f0ee074794ffbd3ec9c2 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Thu, 28 Jan 2021 08:29:38 -1000 Subject: [PATCH 1/2] Properly strip nullable types from this type in optional chain calls --- src/compiler/checker.ts | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 06fc5a6d396f0..b370923cab68f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -27141,6 +27141,16 @@ namespace ts { return getInferredTypes(context); } + function getThisArgumentType(thisArgumentNode: LeftHandSideExpression | undefined) { + if (!thisArgumentNode) { + return voidType; + } + const thisArgumentType = checkExpression(thisArgumentNode); + return isOptionalChainRoot(thisArgumentNode.parent) ? getNonNullableType(thisArgumentType) : + isOptionalChain(thisArgumentNode.parent) ? removeOptionalTypeMarker(thisArgumentType) : + thisArgumentType; + } + function inferTypeArguments(node: CallLikeExpression, signature: Signature, args: readonly Expression[], checkMode: CheckMode, context: InferenceContext): Type[] { if (isJsxOpeningLikeElement(node)) { return inferJsxTypeArguments(node, signature, checkMode, context); @@ -27196,8 +27206,7 @@ namespace ts { const thisType = getThisTypeOfSignature(signature); if (thisType) { const thisArgumentNode = getThisArgumentOfCall(node); - const thisArgumentType = thisArgumentNode ? checkExpression(thisArgumentNode) : voidType; - inferTypes(context.inferences, thisArgumentType, thisType); + inferTypes(context.inferences, getThisArgumentType(thisArgumentNode), thisType); } for (let i = 0; i < argCount; i++) { @@ -27438,20 +27447,7 @@ namespace ts { // If the signature's 'this' type is voidType, then the check is skipped -- anything is compatible. // If the expression is a new expression, then the check is skipped. const thisArgumentNode = getThisArgumentOfCall(node); - let thisArgumentType: Type; - if (thisArgumentNode) { - thisArgumentType = checkExpression(thisArgumentNode); - if (isOptionalChainRoot(thisArgumentNode.parent)) { - thisArgumentType = getNonNullableType(thisArgumentType); - } - else if (isOptionalChain(thisArgumentNode.parent)) { - thisArgumentType = removeOptionalTypeMarker(thisArgumentType); - } - } - else { - thisArgumentType = voidType; - } - + const thisArgumentType = getThisArgumentType(thisArgumentNode); const errorNode = reportErrors ? (thisArgumentNode || node) : undefined; const headMessage = Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1; if (!checkTypeRelatedTo(thisArgumentType, thisType, relation, errorNode, headMessage, containingMessageChain, errorOutputContainer)) { From 1e593d903bae27bd905630c3234826b227069106 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Thu, 28 Jan 2021 08:40:48 -1000 Subject: [PATCH 2/2] Add regression test --- .../baselines/reference/callChainInference.js | 25 ++++++++++++ .../reference/callChainInference.symbols | 39 +++++++++++++++++++ .../reference/callChainInference.types | 37 ++++++++++++++++++ .../callChain/callChainInference.ts | 17 ++++++++ 4 files changed, 118 insertions(+) create mode 100644 tests/baselines/reference/callChainInference.js create mode 100644 tests/baselines/reference/callChainInference.symbols create mode 100644 tests/baselines/reference/callChainInference.types create mode 100644 tests/cases/conformance/expressions/optionalChaining/callChain/callChainInference.ts diff --git a/tests/baselines/reference/callChainInference.js b/tests/baselines/reference/callChainInference.js new file mode 100644 index 0000000000000..f5a77972b27fe --- /dev/null +++ b/tests/baselines/reference/callChainInference.js @@ -0,0 +1,25 @@ +//// [callChainInference.ts] +// Repro from #42404 + +interface Y { + foo(this: T, arg: keyof T): void; + a: number; + b: string; +} + +declare const value: Y | undefined; + +if (value) { + value?.foo("a"); +} + +value?.foo("a"); + + +//// [callChainInference.js] +"use strict"; +// Repro from #42404 +if (value) { + value === null || value === void 0 ? void 0 : value.foo("a"); +} +value === null || value === void 0 ? void 0 : value.foo("a"); diff --git a/tests/baselines/reference/callChainInference.symbols b/tests/baselines/reference/callChainInference.symbols new file mode 100644 index 0000000000000..b1fab1a8d8b93 --- /dev/null +++ b/tests/baselines/reference/callChainInference.symbols @@ -0,0 +1,39 @@ +=== tests/cases/conformance/expressions/optionalChaining/callChain/callChainInference.ts === +// Repro from #42404 + +interface Y { +>Y : Symbol(Y, Decl(callChainInference.ts, 0, 0)) + + foo(this: T, arg: keyof T): void; +>foo : Symbol(Y.foo, Decl(callChainInference.ts, 2, 13)) +>T : Symbol(T, Decl(callChainInference.ts, 3, 8)) +>this : Symbol(this, Decl(callChainInference.ts, 3, 11)) +>T : Symbol(T, Decl(callChainInference.ts, 3, 8)) +>arg : Symbol(arg, Decl(callChainInference.ts, 3, 19)) +>T : Symbol(T, Decl(callChainInference.ts, 3, 8)) + + a: number; +>a : Symbol(Y.a, Decl(callChainInference.ts, 3, 40)) + + b: string; +>b : Symbol(Y.b, Decl(callChainInference.ts, 4, 14)) +} + +declare const value: Y | undefined; +>value : Symbol(value, Decl(callChainInference.ts, 8, 13)) +>Y : Symbol(Y, Decl(callChainInference.ts, 0, 0)) + +if (value) { +>value : Symbol(value, Decl(callChainInference.ts, 8, 13)) + + value?.foo("a"); +>value?.foo : Symbol(Y.foo, Decl(callChainInference.ts, 2, 13)) +>value : Symbol(value, Decl(callChainInference.ts, 8, 13)) +>foo : Symbol(Y.foo, Decl(callChainInference.ts, 2, 13)) +} + +value?.foo("a"); +>value?.foo : Symbol(Y.foo, Decl(callChainInference.ts, 2, 13)) +>value : Symbol(value, Decl(callChainInference.ts, 8, 13)) +>foo : Symbol(Y.foo, Decl(callChainInference.ts, 2, 13)) + diff --git a/tests/baselines/reference/callChainInference.types b/tests/baselines/reference/callChainInference.types new file mode 100644 index 0000000000000..4ed4f5acf6e90 --- /dev/null +++ b/tests/baselines/reference/callChainInference.types @@ -0,0 +1,37 @@ +=== tests/cases/conformance/expressions/optionalChaining/callChain/callChainInference.ts === +// Repro from #42404 + +interface Y { + foo(this: T, arg: keyof T): void; +>foo : (this: T, arg: keyof T) => void +>this : T +>arg : keyof T + + a: number; +>a : number + + b: string; +>b : string +} + +declare const value: Y | undefined; +>value : Y | undefined + +if (value) { +>value : Y | undefined + + value?.foo("a"); +>value?.foo("a") : void +>value?.foo : (this: T, arg: keyof T) => void +>value : Y +>foo : (this: T, arg: keyof T) => void +>"a" : "a" +} + +value?.foo("a"); +>value?.foo("a") : void | undefined +>value?.foo : ((this: T, arg: keyof T) => void) | undefined +>value : Y | undefined +>foo : ((this: T, arg: keyof T) => void) | undefined +>"a" : "a" + diff --git a/tests/cases/conformance/expressions/optionalChaining/callChain/callChainInference.ts b/tests/cases/conformance/expressions/optionalChaining/callChain/callChainInference.ts new file mode 100644 index 0000000000000..52799c1f6200c --- /dev/null +++ b/tests/cases/conformance/expressions/optionalChaining/callChain/callChainInference.ts @@ -0,0 +1,17 @@ +// @strict: true + +// Repro from #42404 + +interface Y { + foo(this: T, arg: keyof T): void; + a: number; + b: string; +} + +declare const value: Y | undefined; + +if (value) { + value?.foo("a"); +} + +value?.foo("a");