Skip to content

Commit 99328e9

Browse files
committed
Propagate 'undefined' instead of the optional type marker at an optional chain boundary
1 parent 556da72 commit 99328e9

File tree

9 files changed

+435
-59
lines changed

9 files changed

+435
-59
lines changed

src/compiler/binder.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,10 +1556,6 @@ namespace ts {
15561556
}
15571557
}
15581558

1559-
function isOutermostOptionalChain(node: OptionalChain) {
1560-
return !isOptionalChain(node.parent) || isOptionalChainRoot(node.parent) || node !== node.parent.expression;
1561-
}
1562-
15631559
function bindOptionalExpression(node: Expression, trueTarget: FlowLabel, falseTarget: FlowLabel) {
15641560
doWithConditionalBranches(bind, node, trueTarget, falseTarget);
15651561
if (!isOptionalChain(node) || isOutermostOptionalChain(node)) {

src/compiler/checker.ts

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8639,14 +8639,23 @@ namespace ts {
86398639
return result;
86408640
}
86418641

8642-
function getOptionalCallSignature(signature: Signature) {
8643-
return signatureIsOptionalCall(signature) ? signature :
8644-
(signature.optionalCallSignatureCache || (signature.optionalCallSignatureCache = createOptionalCallSignature(signature)));
8642+
function getOptionalCallSignature(signature: Signature, callChainFlags: SignatureFlags): Signature {
8643+
if ((signature.flags & SignatureFlags.CallChainFlags) === callChainFlags) {
8644+
return signature;
8645+
}
8646+
if (!signature.optionalCallSignatureCache) {
8647+
signature.optionalCallSignatureCache = {};
8648+
}
8649+
const key = callChainFlags === SignatureFlags.IsInnerCallChain ? "inner" : "outer";
8650+
return signature.optionalCallSignatureCache[key]
8651+
|| (signature.optionalCallSignatureCache[key] = createOptionalCallSignature(signature, callChainFlags));
86458652
}
86468653

8647-
function createOptionalCallSignature(signature: Signature) {
8654+
function createOptionalCallSignature(signature: Signature, callChainFlags: SignatureFlags) {
8655+
Debug.assert(callChainFlags === SignatureFlags.IsInnerCallChain || callChainFlags === SignatureFlags.IsOuterCallChain,
8656+
"An optional call signature can either be for an inner call chain or an outer call chain, but not both.");
86488657
const result = cloneSignature(signature);
8649-
result.flags |= SignatureFlags.IsOptionalCall;
8658+
result.flags |= callChainFlags;
86508659
return result;
86518660
}
86528661

@@ -10261,9 +10270,12 @@ namespace ts {
1026110270
signature.unionSignatures ? getUnionType(map(signature.unionSignatures, getReturnTypeOfSignature), UnionReduction.Subtype) :
1026210271
getReturnTypeFromAnnotation(signature.declaration!) ||
1026310272
(nodeIsMissing((<FunctionLikeDeclaration>signature.declaration).body) ? anyType : getReturnTypeFromBody(<FunctionLikeDeclaration>signature.declaration));
10264-
if (signatureIsOptionalCall(signature)) {
10273+
if (signature.flags & SignatureFlags.IsInnerCallChain) {
1026510274
type = addOptionalTypeMarker(type);
1026610275
}
10276+
else if (signature.flags & SignatureFlags.IsOuterCallChain) {
10277+
type = getOptionalType(type);
10278+
}
1026710279
if (!popTypeResolution()) {
1026810280
if (signature.declaration) {
1026910281
const typeNode = getEffectiveReturnTypeNode(signature.declaration);
@@ -16690,8 +16702,8 @@ namespace ts {
1669016702
return strictNullChecks ? filterType(type, isNotOptionalTypeMarker) : type;
1669116703
}
1669216704

16693-
function propagateOptionalTypeMarker(type: Type, wasOptional: boolean) {
16694-
return wasOptional ? addOptionalTypeMarker(type) : type;
16705+
function propagateOptionalTypeMarker(type: Type, node: OptionalChain, wasOptional: boolean) {
16706+
return wasOptional ? isOutermostOptionalChain(node) ? getOptionalType(type) : addOptionalTypeMarker(type) : type;
1669516707
}
1669616708

1669716709
function getOptionalExpressionType(exprType: Type, expression: Expression) {
@@ -22730,7 +22742,7 @@ namespace ts {
2273022742
function checkPropertyAccessChain(node: PropertyAccessChain) {
2273122743
const leftType = checkExpression(node.expression);
2273222744
const nonOptionalType = getOptionalExpressionType(leftType, node.expression);
22733-
return propagateOptionalTypeMarker(checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullType(nonOptionalType, node.expression), node.name), nonOptionalType !== leftType);
22745+
return propagateOptionalTypeMarker(checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullType(nonOptionalType, node.expression), node.name), node, nonOptionalType !== leftType);
2273422746
}
2273522747

2273622748
function checkQualifiedName(node: QualifiedName) {
@@ -23162,7 +23174,7 @@ namespace ts {
2316223174
function checkElementAccessChain(node: ElementAccessChain) {
2316323175
const exprType = checkExpression(node.expression);
2316423176
const nonOptionalType = getOptionalExpressionType(exprType, node.expression);
23165-
return propagateOptionalTypeMarker(checkElementAccessExpression(node, checkNonNullType(nonOptionalType, node.expression)), nonOptionalType !== exprType);
23177+
return propagateOptionalTypeMarker(checkElementAccessExpression(node, checkNonNullType(nonOptionalType, node.expression)), node, nonOptionalType !== exprType);
2316623178
}
2316723179

2316823180
function checkElementAccessExpression(node: ElementAccessExpression, exprType: Type): Type {
@@ -23267,7 +23279,7 @@ namespace ts {
2326723279
// interface B extends A { (x: 'foo'): string }
2326823280
// const b: B;
2326923281
// b('foo') // <- here overloads should be processed as [(x:'foo'): string, (x: string): void]
23270-
function reorderCandidates(signatures: readonly Signature[], result: Signature[], isOptionalCall: boolean): void {
23282+
function reorderCandidates(signatures: readonly Signature[], result: Signature[], callChainFlags: SignatureFlags): void {
2327123283
let lastParent: Node | undefined;
2327223284
let lastSymbol: Symbol | undefined;
2327323285
let cutoffIndex = 0;
@@ -23309,7 +23321,7 @@ namespace ts {
2330923321
spliceIndex = index;
2331023322
}
2331123323

23312-
result.splice(spliceIndex, 0, isOptionalCall ? getOptionalCallSignature(signature) : signature);
23324+
result.splice(spliceIndex, 0, callChainFlags ? getOptionalCallSignature(signature, callChainFlags) : signature);
2331323325
}
2331423326
}
2331523327

@@ -23975,7 +23987,7 @@ namespace ts {
2397523987
return createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.Expected_0_type_arguments_but_got_1, belowArgCount === -Infinity ? aboveArgCount : belowArgCount, argCount);
2397623988
}
2397723989

23978-
function resolveCall(node: CallLikeExpression, signatures: readonly Signature[], candidatesOutArray: Signature[] | undefined, checkMode: CheckMode, isOptionalCall: boolean, fallbackError?: DiagnosticMessage): Signature {
23990+
function resolveCall(node: CallLikeExpression, signatures: readonly Signature[], candidatesOutArray: Signature[] | undefined, checkMode: CheckMode, callChainFlags: SignatureFlags, fallbackError?: DiagnosticMessage): Signature {
2397923991
const isTaggedTemplate = node.kind === SyntaxKind.TaggedTemplateExpression;
2398023992
const isDecorator = node.kind === SyntaxKind.Decorator;
2398123993
const isJsxOpeningOrSelfClosingElement = isJsxOpeningLikeElement(node);
@@ -23994,7 +24006,7 @@ namespace ts {
2399424006

2399524007
const candidates = candidatesOutArray || [];
2399624008
// reorderCandidates fills up the candidates array directly
23997-
reorderCandidates(signatures, candidates, isOptionalCall);
24009+
reorderCandidates(signatures, candidates, callChainFlags);
2399824010
if (!candidates.length) {
2399924011
if (reportErrors) {
2400024012
diagnostics.add(getDiagnosticForCallNode(node, Diagnostics.Call_target_does_not_contain_any_signatures));
@@ -24381,22 +24393,25 @@ namespace ts {
2438124393
const baseTypeNode = getEffectiveBaseTypeNode(getContainingClass(node)!);
2438224394
if (baseTypeNode) {
2438324395
const baseConstructors = getInstantiatedConstructorsForTypeArguments(superType, baseTypeNode.typeArguments, baseTypeNode);
24384-
return resolveCall(node, baseConstructors, candidatesOutArray, checkMode, /*isOptional*/ false);
24396+
return resolveCall(node, baseConstructors, candidatesOutArray, checkMode, SignatureFlags.None);
2438524397
}
2438624398
}
2438724399
return resolveUntypedCall(node);
2438824400
}
2438924401

24390-
let isOptional: boolean;
24402+
let callChainFlags: SignatureFlags;
2439124403
let funcType = checkExpression(node.expression);
2439224404
if (isCallChain(node)) {
2439324405
const nonOptionalType = getOptionalExpressionType(funcType, node.expression);
24394-
isOptional = nonOptionalType !== funcType;
24406+
callChainFlags = nonOptionalType === funcType ? SignatureFlags.None :
24407+
isOutermostOptionalChain(node) ? SignatureFlags.IsOuterCallChain :
24408+
SignatureFlags.IsInnerCallChain;
2439524409
funcType = nonOptionalType;
2439624410
}
2439724411
else {
24398-
isOptional = false;
24412+
callChainFlags = SignatureFlags.None;
2439924413
}
24414+
2440024415
funcType = checkNonNullTypeWithReporter(
2440124416
funcType,
2440224417
node.expression,
@@ -24472,7 +24487,7 @@ namespace ts {
2447224487
return resolveErrorCall(node);
2447324488
}
2447424489

24475-
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, isOptional);
24490+
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, callChainFlags);
2447624491
}
2447724492

2447824493
function isGenericFunctionReturningFunction(signature: Signature) {
@@ -24543,7 +24558,7 @@ namespace ts {
2454324558
return resolveErrorCall(node);
2454424559
}
2454524560

24546-
return resolveCall(node, constructSignatures, candidatesOutArray, checkMode, /*isOptional*/ false);
24561+
return resolveCall(node, constructSignatures, candidatesOutArray, checkMode, SignatureFlags.None);
2454724562
}
2454824563

2454924564
// If expressionType's apparent type is an object type with no construct signatures but
@@ -24552,7 +24567,7 @@ namespace ts {
2455224567
// operation is Any. It is an error to have a Void this type.
2455324568
const callSignatures = getSignaturesOfType(expressionType, SignatureKind.Call);
2455424569
if (callSignatures.length) {
24555-
const signature = resolveCall(node, callSignatures, candidatesOutArray, checkMode, /*isOptional*/ false);
24570+
const signature = resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None);
2455624571
if (!noImplicitAny) {
2455724572
if (signature.declaration && !isJSConstructor(signature.declaration) && getReturnTypeOfSignature(signature) !== voidType) {
2455824573
error(node, Diagnostics.Only_a_void_function_can_be_called_with_the_new_keyword);
@@ -24767,7 +24782,7 @@ namespace ts {
2476724782
return resolveErrorCall(node);
2476824783
}
2476924784

24770-
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, /*isOptional*/ false);
24785+
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None);
2477124786
}
2477224787

2477324788
/**
@@ -24830,7 +24845,7 @@ namespace ts {
2483024845
return resolveErrorCall(node);
2483124846
}
2483224847

24833-
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, /*isOptional*/ false, headMessage);
24848+
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None, headMessage);
2483424849
}
2483524850

2483624851
function createSignatureForJSXIntrinsic(node: JsxOpeningLikeElement, result: Type): Signature {
@@ -24882,7 +24897,7 @@ namespace ts {
2488224897
return resolveErrorCall(node);
2488324898
}
2488424899

24885-
return resolveCall(node, signatures, candidatesOutArray, checkMode, /*isOptional*/ false);
24900+
return resolveCall(node, signatures, candidatesOutArray, checkMode, SignatureFlags.None);
2488624901
}
2488724902

2488824903
/**
@@ -27355,6 +27370,20 @@ namespace ts {
2735527370
}
2735627371
}
2735727372

27373+
function getReturnTypeOfSingleNonGenericCallSignature(funcType: Type) {
27374+
const signature = getSingleCallSignature(funcType);
27375+
if (signature && !signature.typeParameters) {
27376+
return getReturnTypeOfSignature(signature);
27377+
}
27378+
}
27379+
27380+
function getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr: CallChain) {
27381+
const funcType = checkExpression(expr.expression);
27382+
const nonOptionalType = getOptionalExpressionType(funcType, expr.expression);
27383+
const returnType = getReturnTypeOfSingleNonGenericCallSignature(funcType);
27384+
return returnType && propagateOptionalTypeMarker(returnType, expr, nonOptionalType !== funcType);
27385+
}
27386+
2735827387
/**
2735927388
* Returns the type of an expression. Unlike checkExpression, this function is simply concerned
2736027389
* with computing the type and may not fully check all contained sub-expressions for errors.
@@ -27366,21 +27395,10 @@ namespace ts {
2736627395
// Optimize for the common case of a call to a function with a single non-generic call
2736727396
// signature where we can just fetch the return type without checking the arguments.
2736827397
if (isCallExpression(expr) && expr.expression.kind !== SyntaxKind.SuperKeyword && !isRequireCall(expr, /*checkArgumentIsStringLiteralLike*/ true) && !isSymbolOrSymbolForCall(expr)) {
27369-
let isOptional: boolean;
27370-
let funcType: Type;
27371-
if (isCallChain(expr)) {
27372-
funcType = checkExpression(expr.expression);
27373-
const nonOptionalType = getOptionalExpressionType(funcType, expr.expression);
27374-
isOptional = funcType !== nonOptionalType;
27375-
funcType = checkNonNullType(nonOptionalType, expr.expression);
27376-
}
27377-
else {
27378-
isOptional = false;
27379-
funcType = checkNonNullExpression(expr.expression);
27380-
}
27381-
const signature = getSingleCallSignature(funcType);
27382-
if (signature && !signature.typeParameters) {
27383-
return propagateOptionalTypeMarker(getReturnTypeOfSignature(signature), isOptional);
27398+
const type = isCallChain(expr) ? getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr) :
27399+
getReturnTypeOfSingleNonGenericCallSignature(checkNonNullExpression(expr.expression));
27400+
if (type) {
27401+
return type;
2738427402
}
2738527403
}
2738627404
else if (isAssertionExpression(expr) && !isConstTypeReference(expr.type)) {
@@ -36093,7 +36111,4 @@ namespace ts {
3609336111
return !!(s.flags & SignatureFlags.HasLiteralTypes);
3609436112
}
3609536113

36096-
export function signatureIsOptionalCall(s: Signature) {
36097-
return !!(s.flags & SignatureFlags.IsOptionalCall);
36098-
}
3609936114
}

src/compiler/types.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4673,14 +4673,17 @@ namespace ts {
46734673
/* @internal */
46744674
export const enum SignatureFlags {
46754675
None = 0,
4676-
HasRestParameter = 1 << 0, // Indicates last parameter is rest parameter
4677-
HasLiteralTypes = 1 << 1, // Indicates signature is specialized
4678-
IsOptionalCall = 1 << 2, // Indicates signature comes from a CallChain
4676+
HasRestParameter = 1 << 0, // Indicates last parameter is rest parameter
4677+
HasLiteralTypes = 1 << 1, // Indicates signature is specialized
4678+
IsInnerCallChain = 1 << 2, // Indicates signature comes from a CallChain nested in an outer OptionalChain
4679+
IsOuterCallChain = 1 << 3, // Indicates signature comes from a CallChain thet is the outermost chain of an optional expression
46794680

4680-
// We do not propagate `IsOptionalCall` to instantiated signatures, as that would result in us
4681+
// We do not propagate `IsInnerCallChain` to instantiated signatures, as that would result in us
46814682
// attempting to add `| undefined` on each recursive call to `getReturnTypeOfSignature` when
46824683
// instantiating the return type.
46834684
PropagatingFlags = HasRestParameter | HasLiteralTypes,
4685+
4686+
CallChainFlags = IsInnerCallChain | IsOuterCallChain,
46844687
}
46854688

46864689
export interface Signature {
@@ -4712,7 +4715,7 @@ namespace ts {
47124715
/* @internal */
47134716
canonicalSignatureCache?: Signature; // Canonical version of signature (deferred)
47144717
/* @internal */
4715-
optionalCallSignatureCache?: Signature; // Optional chained call version of signature (deferred)
4718+
optionalCallSignatureCache?: { inner?: Signature, outer?: Signature }; // Optional chained call version of signature (deferred)
47164719
/* @internal */
47174720
isolatedSignatureType?: ObjectType; // A manufactured type that just contains the signature for purposes of signature comparison
47184721
/* @internal */

src/compiler/utilities.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5911,6 +5911,11 @@ namespace ts {
59115911
|| kind === SyntaxKind.CallExpression);
59125912
}
59135913

5914+
/* @internal */
5915+
export function isOptionalChainRoot(node: Node): node is OptionalChainRoot {
5916+
return isOptionalChain(node) && !!node.questionDotToken;
5917+
}
5918+
59145919
/**
59155920
* Determines whether a node is the expression preceding an optional chain (i.e. `a` in `a?.b`).
59165921
*/
@@ -5919,6 +5924,23 @@ namespace ts {
59195924
return isOptionalChainRoot(node.parent) && node.parent.expression === node;
59205925
}
59215926

5927+
/**
5928+
* Determines whether a node is the outermost `OptionalChain` in an ECMAScript `OptionalExpression`:
5929+
*
5930+
* 1. For `a?.b.c`, the outermost chain is `a?.b.c` (`c` is the end of the chain starting at `a?.`)
5931+
* 2. For `(a?.b.c).d`, the outermost chain is `a?.b.c` (`c` is the end of the chain starting at `a?.` since parens end the chain)
5932+
* 3. For `a?.b.c?.d`, both `a?.b.c` and `a?.b.c?.d` are outermost (`c` is the end of the chain starting at `a?.`, and `d` is
5933+
* the end of the chain starting at `c?.`)
5934+
* 4. For `a?.(b?.c).d`, both `b?.c` and `a?.(b?.c)d` are outermost (`c` is the end of the chain starting at `b`, and `d` is
5935+
* the end of the chain starting at `a?.`)
5936+
*/
5937+
/* @internal */
5938+
export function isOutermostOptionalChain(node: OptionalChain) {
5939+
return !isOptionalChain(node.parent) // cases 1 and 2
5940+
|| isOptionalChainRoot(node.parent) // case 3
5941+
|| node !== node.parent.expression; // case 4
5942+
}
5943+
59225944
export function isNewExpression(node: Node): node is NewExpression {
59235945
return node.kind === SyntaxKind.NewExpression;
59245946
}
@@ -7236,11 +7258,6 @@ namespace ts {
72367258
return node.kind === SyntaxKind.GetAccessor;
72377259
}
72387260

7239-
/* @internal */
7240-
export function isOptionalChainRoot(node: Node): node is OptionalChainRoot {
7241-
return isOptionalChain(node) && !!node.questionDotToken;
7242-
}
7243-
72447261
/** True if has jsdoc nodes attached to it. */
72457262
/* @internal */
72467263
// TODO: GH#19856 Would like to return `node is Node & { jsDoc: JSDoc[] }` but it causes long compile times

tests/baselines/reference/callChain.3.types

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ const n4: number | undefined = a?.m?.({x: absorb()}); // likewise
4545
>a?.m : (<T>(obj: { x: T; }) => T) | undefined
4646
>a : { m?<T>(obj: { x: T; }): T; } | undefined
4747
>m : (<T>(obj: { x: T; }) => T) | undefined
48-
>{x: absorb()} : { x: number | undefined; }
49-
>x : number | undefined
50-
>absorb() : number | undefined
48+
>{x: absorb()} : { x: number; }
49+
>x : number
50+
>absorb() : number
5151
>absorb : <T>() => T
5252

5353
// Also a test showing `!` vs `?` for good measure

0 commit comments

Comments
 (0)