Skip to content

Commit ba5e86f

Browse files
rbucktonsandersn
andauthored
Propagate 'undefined' instead of the optional type marker at an optional chain boundary (microsoft#34588)
* Propagate 'undefined' instead of the optional type marker at an optional chain boundary * Update src/compiler/types.ts Co-Authored-By: Nathan Shively-Sanders <[email protected]>
1 parent ec367fe commit ba5e86f

File tree

9 files changed

+435
-59
lines changed

9 files changed

+435
-59
lines changed

src/compiler/binder.ts

-4
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

+57-42
Original file line numberDiff line numberDiff line change
@@ -8691,14 +8691,23 @@ namespace ts {
86918691
return result;
86928692
}
86938693

8694-
function getOptionalCallSignature(signature: Signature) {
8695-
return signatureIsOptionalCall(signature) ? signature :
8696-
(signature.optionalCallSignatureCache || (signature.optionalCallSignatureCache = createOptionalCallSignature(signature)));
8694+
function getOptionalCallSignature(signature: Signature, callChainFlags: SignatureFlags): Signature {
8695+
if ((signature.flags & SignatureFlags.CallChainFlags) === callChainFlags) {
8696+
return signature;
8697+
}
8698+
if (!signature.optionalCallSignatureCache) {
8699+
signature.optionalCallSignatureCache = {};
8700+
}
8701+
const key = callChainFlags === SignatureFlags.IsInnerCallChain ? "inner" : "outer";
8702+
return signature.optionalCallSignatureCache[key]
8703+
|| (signature.optionalCallSignatureCache[key] = createOptionalCallSignature(signature, callChainFlags));
86978704
}
86988705

8699-
function createOptionalCallSignature(signature: Signature) {
8706+
function createOptionalCallSignature(signature: Signature, callChainFlags: SignatureFlags) {
8707+
Debug.assert(callChainFlags === SignatureFlags.IsInnerCallChain || callChainFlags === SignatureFlags.IsOuterCallChain,
8708+
"An optional call signature can either be for an inner call chain or an outer call chain, but not both.");
87008709
const result = cloneSignature(signature);
8701-
result.flags |= SignatureFlags.IsOptionalCall;
8710+
result.flags |= callChainFlags;
87028711
return result;
87038712
}
87048713

@@ -10313,9 +10322,12 @@ namespace ts {
1031310322
signature.unionSignatures ? getUnionType(map(signature.unionSignatures, getReturnTypeOfSignature), UnionReduction.Subtype) :
1031410323
getReturnTypeFromAnnotation(signature.declaration!) ||
1031510324
(nodeIsMissing((<FunctionLikeDeclaration>signature.declaration).body) ? anyType : getReturnTypeFromBody(<FunctionLikeDeclaration>signature.declaration));
10316-
if (signatureIsOptionalCall(signature)) {
10325+
if (signature.flags & SignatureFlags.IsInnerCallChain) {
1031710326
type = addOptionalTypeMarker(type);
1031810327
}
10328+
else if (signature.flags & SignatureFlags.IsOuterCallChain) {
10329+
type = getOptionalType(type);
10330+
}
1031910331
if (!popTypeResolution()) {
1032010332
if (signature.declaration) {
1032110333
const typeNode = getEffectiveReturnTypeNode(signature.declaration);
@@ -16767,8 +16779,8 @@ namespace ts {
1676716779
return strictNullChecks ? filterType(type, isNotOptionalTypeMarker) : type;
1676816780
}
1676916781

16770-
function propagateOptionalTypeMarker(type: Type, wasOptional: boolean) {
16771-
return wasOptional ? addOptionalTypeMarker(type) : type;
16782+
function propagateOptionalTypeMarker(type: Type, node: OptionalChain, wasOptional: boolean) {
16783+
return wasOptional ? isOutermostOptionalChain(node) ? getOptionalType(type) : addOptionalTypeMarker(type) : type;
1677216784
}
1677316785

1677416786
function getOptionalExpressionType(exprType: Type, expression: Expression) {
@@ -22835,7 +22847,7 @@ namespace ts {
2283522847
function checkPropertyAccessChain(node: PropertyAccessChain) {
2283622848
const leftType = checkExpression(node.expression);
2283722849
const nonOptionalType = getOptionalExpressionType(leftType, node.expression);
22838-
return propagateOptionalTypeMarker(checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullType(nonOptionalType, node.expression), node.name), nonOptionalType !== leftType);
22850+
return propagateOptionalTypeMarker(checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullType(nonOptionalType, node.expression), node.name), node, nonOptionalType !== leftType);
2283922851
}
2284022852

2284122853
function checkQualifiedName(node: QualifiedName) {
@@ -23267,7 +23279,7 @@ namespace ts {
2326723279
function checkElementAccessChain(node: ElementAccessChain) {
2326823280
const exprType = checkExpression(node.expression);
2326923281
const nonOptionalType = getOptionalExpressionType(exprType, node.expression);
23270-
return propagateOptionalTypeMarker(checkElementAccessExpression(node, checkNonNullType(nonOptionalType, node.expression)), nonOptionalType !== exprType);
23282+
return propagateOptionalTypeMarker(checkElementAccessExpression(node, checkNonNullType(nonOptionalType, node.expression)), node, nonOptionalType !== exprType);
2327123283
}
2327223284

2327323285
function checkElementAccessExpression(node: ElementAccessExpression, exprType: Type): Type {
@@ -23372,7 +23384,7 @@ namespace ts {
2337223384
// interface B extends A { (x: 'foo'): string }
2337323385
// const b: B;
2337423386
// b('foo') // <- here overloads should be processed as [(x:'foo'): string, (x: string): void]
23375-
function reorderCandidates(signatures: readonly Signature[], result: Signature[], isOptionalCall: boolean): void {
23387+
function reorderCandidates(signatures: readonly Signature[], result: Signature[], callChainFlags: SignatureFlags): void {
2337623388
let lastParent: Node | undefined;
2337723389
let lastSymbol: Symbol | undefined;
2337823390
let cutoffIndex = 0;
@@ -23414,7 +23426,7 @@ namespace ts {
2341423426
spliceIndex = index;
2341523427
}
2341623428

23417-
result.splice(spliceIndex, 0, isOptionalCall ? getOptionalCallSignature(signature) : signature);
23429+
result.splice(spliceIndex, 0, callChainFlags ? getOptionalCallSignature(signature, callChainFlags) : signature);
2341823430
}
2341923431
}
2342023432

@@ -24080,7 +24092,7 @@ namespace ts {
2408024092
return createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.Expected_0_type_arguments_but_got_1, belowArgCount === -Infinity ? aboveArgCount : belowArgCount, argCount);
2408124093
}
2408224094

24083-
function resolveCall(node: CallLikeExpression, signatures: readonly Signature[], candidatesOutArray: Signature[] | undefined, checkMode: CheckMode, isOptionalCall: boolean, fallbackError?: DiagnosticMessage): Signature {
24095+
function resolveCall(node: CallLikeExpression, signatures: readonly Signature[], candidatesOutArray: Signature[] | undefined, checkMode: CheckMode, callChainFlags: SignatureFlags, fallbackError?: DiagnosticMessage): Signature {
2408424096
const isTaggedTemplate = node.kind === SyntaxKind.TaggedTemplateExpression;
2408524097
const isDecorator = node.kind === SyntaxKind.Decorator;
2408624098
const isJsxOpeningOrSelfClosingElement = isJsxOpeningLikeElement(node);
@@ -24099,7 +24111,7 @@ namespace ts {
2409924111

2410024112
const candidates = candidatesOutArray || [];
2410124113
// reorderCandidates fills up the candidates array directly
24102-
reorderCandidates(signatures, candidates, isOptionalCall);
24114+
reorderCandidates(signatures, candidates, callChainFlags);
2410324115
if (!candidates.length) {
2410424116
if (reportErrors) {
2410524117
diagnostics.add(getDiagnosticForCallNode(node, Diagnostics.Call_target_does_not_contain_any_signatures));
@@ -24486,22 +24498,25 @@ namespace ts {
2448624498
const baseTypeNode = getEffectiveBaseTypeNode(getContainingClass(node)!);
2448724499
if (baseTypeNode) {
2448824500
const baseConstructors = getInstantiatedConstructorsForTypeArguments(superType, baseTypeNode.typeArguments, baseTypeNode);
24489-
return resolveCall(node, baseConstructors, candidatesOutArray, checkMode, /*isOptional*/ false);
24501+
return resolveCall(node, baseConstructors, candidatesOutArray, checkMode, SignatureFlags.None);
2449024502
}
2449124503
}
2449224504
return resolveUntypedCall(node);
2449324505
}
2449424506

24495-
let isOptional: boolean;
24507+
let callChainFlags: SignatureFlags;
2449624508
let funcType = checkExpression(node.expression);
2449724509
if (isCallChain(node)) {
2449824510
const nonOptionalType = getOptionalExpressionType(funcType, node.expression);
24499-
isOptional = nonOptionalType !== funcType;
24511+
callChainFlags = nonOptionalType === funcType ? SignatureFlags.None :
24512+
isOutermostOptionalChain(node) ? SignatureFlags.IsOuterCallChain :
24513+
SignatureFlags.IsInnerCallChain;
2450024514
funcType = nonOptionalType;
2450124515
}
2450224516
else {
24503-
isOptional = false;
24517+
callChainFlags = SignatureFlags.None;
2450424518
}
24519+
2450524520
funcType = checkNonNullTypeWithReporter(
2450624521
funcType,
2450724522
node.expression,
@@ -24577,7 +24592,7 @@ namespace ts {
2457724592
return resolveErrorCall(node);
2457824593
}
2457924594

24580-
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, isOptional);
24595+
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, callChainFlags);
2458124596
}
2458224597

2458324598
function isGenericFunctionReturningFunction(signature: Signature) {
@@ -24648,7 +24663,7 @@ namespace ts {
2464824663
return resolveErrorCall(node);
2464924664
}
2465024665

24651-
return resolveCall(node, constructSignatures, candidatesOutArray, checkMode, /*isOptional*/ false);
24666+
return resolveCall(node, constructSignatures, candidatesOutArray, checkMode, SignatureFlags.None);
2465224667
}
2465324668

2465424669
// If expressionType's apparent type is an object type with no construct signatures but
@@ -24657,7 +24672,7 @@ namespace ts {
2465724672
// operation is Any. It is an error to have a Void this type.
2465824673
const callSignatures = getSignaturesOfType(expressionType, SignatureKind.Call);
2465924674
if (callSignatures.length) {
24660-
const signature = resolveCall(node, callSignatures, candidatesOutArray, checkMode, /*isOptional*/ false);
24675+
const signature = resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None);
2466124676
if (!noImplicitAny) {
2466224677
if (signature.declaration && !isJSConstructor(signature.declaration) && getReturnTypeOfSignature(signature) !== voidType) {
2466324678
error(node, Diagnostics.Only_a_void_function_can_be_called_with_the_new_keyword);
@@ -24872,7 +24887,7 @@ namespace ts {
2487224887
return resolveErrorCall(node);
2487324888
}
2487424889

24875-
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, /*isOptional*/ false);
24890+
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None);
2487624891
}
2487724892

2487824893
/**
@@ -24935,7 +24950,7 @@ namespace ts {
2493524950
return resolveErrorCall(node);
2493624951
}
2493724952

24938-
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, /*isOptional*/ false, headMessage);
24953+
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None, headMessage);
2493924954
}
2494024955

2494124956
function createSignatureForJSXIntrinsic(node: JsxOpeningLikeElement, result: Type): Signature {
@@ -24987,7 +25002,7 @@ namespace ts {
2498725002
return resolveErrorCall(node);
2498825003
}
2498925004

24990-
return resolveCall(node, signatures, candidatesOutArray, checkMode, /*isOptional*/ false);
25005+
return resolveCall(node, signatures, candidatesOutArray, checkMode, SignatureFlags.None);
2499125006
}
2499225007

2499325008
/**
@@ -27460,6 +27475,20 @@ namespace ts {
2746027475
}
2746127476
}
2746227477

27478+
function getReturnTypeOfSingleNonGenericCallSignature(funcType: Type) {
27479+
const signature = getSingleCallSignature(funcType);
27480+
if (signature && !signature.typeParameters) {
27481+
return getReturnTypeOfSignature(signature);
27482+
}
27483+
}
27484+
27485+
function getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr: CallChain) {
27486+
const funcType = checkExpression(expr.expression);
27487+
const nonOptionalType = getOptionalExpressionType(funcType, expr.expression);
27488+
const returnType = getReturnTypeOfSingleNonGenericCallSignature(funcType);
27489+
return returnType && propagateOptionalTypeMarker(returnType, expr, nonOptionalType !== funcType);
27490+
}
27491+
2746327492
/**
2746427493
* Returns the type of an expression. Unlike checkExpression, this function is simply concerned
2746527494
* with computing the type and may not fully check all contained sub-expressions for errors.
@@ -27471,21 +27500,10 @@ namespace ts {
2747127500
// Optimize for the common case of a call to a function with a single non-generic call
2747227501
// signature where we can just fetch the return type without checking the arguments.
2747327502
if (isCallExpression(expr) && expr.expression.kind !== SyntaxKind.SuperKeyword && !isRequireCall(expr, /*checkArgumentIsStringLiteralLike*/ true) && !isSymbolOrSymbolForCall(expr)) {
27474-
let isOptional: boolean;
27475-
let funcType: Type;
27476-
if (isCallChain(expr)) {
27477-
funcType = checkExpression(expr.expression);
27478-
const nonOptionalType = getOptionalExpressionType(funcType, expr.expression);
27479-
isOptional = funcType !== nonOptionalType;
27480-
funcType = checkNonNullType(nonOptionalType, expr.expression);
27481-
}
27482-
else {
27483-
isOptional = false;
27484-
funcType = checkNonNullExpression(expr.expression);
27485-
}
27486-
const signature = getSingleCallSignature(funcType);
27487-
if (signature && !signature.typeParameters) {
27488-
return propagateOptionalTypeMarker(getReturnTypeOfSignature(signature), isOptional);
27503+
const type = isCallChain(expr) ? getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr) :
27504+
getReturnTypeOfSingleNonGenericCallSignature(checkNonNullExpression(expr.expression));
27505+
if (type) {
27506+
return type;
2748927507
}
2749027508
}
2749127509
else if (isAssertionExpression(expr) && !isConstTypeReference(expr.type)) {
@@ -36198,7 +36216,4 @@ namespace ts {
3619836216
return !!(s.flags & SignatureFlags.HasLiteralTypes);
3619936217
}
3620036218

36201-
export function signatureIsOptionalCall(s: Signature) {
36202-
return !!(s.flags & SignatureFlags.IsOptionalCall);
36203-
}
3620436219
}

src/compiler/types.ts

+8-5
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 that 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

+22-5
Original file line numberDiff line numberDiff line change
@@ -5947,6 +5947,11 @@ namespace ts {
59475947
|| kind === SyntaxKind.CallExpression);
59485948
}
59495949

5950+
/* @internal */
5951+
export function isOptionalChainRoot(node: Node): node is OptionalChainRoot {
5952+
return isOptionalChain(node) && !!node.questionDotToken;
5953+
}
5954+
59505955
/**
59515956
* Determines whether a node is the expression preceding an optional chain (i.e. `a` in `a?.b`).
59525957
*/
@@ -5955,6 +5960,23 @@ namespace ts {
59555960
return isOptionalChainRoot(node.parent) && node.parent.expression === node;
59565961
}
59575962

5963+
/**
5964+
* Determines whether a node is the outermost `OptionalChain` in an ECMAScript `OptionalExpression`:
5965+
*
5966+
* 1. For `a?.b.c`, the outermost chain is `a?.b.c` (`c` is the end of the chain starting at `a?.`)
5967+
* 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)
5968+
* 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
5969+
* the end of the chain starting at `c?.`)
5970+
* 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
5971+
* the end of the chain starting at `a?.`)
5972+
*/
5973+
/* @internal */
5974+
export function isOutermostOptionalChain(node: OptionalChain) {
5975+
return !isOptionalChain(node.parent) // cases 1 and 2
5976+
|| isOptionalChainRoot(node.parent) // case 3
5977+
|| node !== node.parent.expression; // case 4
5978+
}
5979+
59585980
export function isNullishCoalesce(node: Node) {
59595981
return node.kind === SyntaxKind.BinaryExpression && (<BinaryExpression>node).operatorToken.kind === SyntaxKind.QuestionQuestionToken;
59605982
}
@@ -7276,11 +7298,6 @@ namespace ts {
72767298
return node.kind === SyntaxKind.GetAccessor;
72777299
}
72787300

7279-
/* @internal */
7280-
export function isOptionalChainRoot(node: Node): node is OptionalChainRoot {
7281-
return isOptionalChain(node) && !!node.questionDotToken;
7282-
}
7283-
72847301
/** True if has jsdoc nodes attached to it. */
72857302
/* @internal */
72867303
// TODO: GH#19856 Would like to return `node is Node & { jsDoc: JSDoc[] }` but it causes long compile times

tests/baselines/reference/callChain.3.types

+3-3
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)