Skip to content

Commit 30979c2

Browse files
authored
Narrow generic conditional and indexed access return types when checking return statements (#56941)
1 parent 5e2e321 commit 30979c2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+11876
-42
lines changed

src/compiler/binder.ts

+14
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
543543
var preSwitchCaseFlow: FlowNode | undefined;
544544
var activeLabelList: ActiveLabel | undefined;
545545
var hasExplicitReturn: boolean;
546+
var inReturnPosition: boolean;
546547
var hasFlowEffects: boolean;
547548

548549
// state used for emit helpers
@@ -622,6 +623,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
622623
currentExceptionTarget = undefined;
623624
activeLabelList = undefined;
624625
hasExplicitReturn = false;
626+
inReturnPosition = false;
625627
hasFlowEffects = false;
626628
inAssignmentPattern = false;
627629
emitFlags = NodeFlags.None;
@@ -967,7 +969,9 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
967969
const saveContainer = container;
968970
const saveThisParentContainer = thisParentContainer;
969971
const savedBlockScopeContainer = blockScopeContainer;
972+
const savedInReturnPosition = inReturnPosition;
970973

974+
if (node.kind === SyntaxKind.ArrowFunction && node.body.kind !== SyntaxKind.Block) inReturnPosition = true;
971975
// Depending on what kind of node this is, we may have to adjust the current container
972976
// and block-container. If the current node is a container, then it is automatically
973977
// considered the current block-container as well. Also, for containers that we know
@@ -1071,6 +1075,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
10711075
bindChildren(node);
10721076
}
10731077

1078+
inReturnPosition = savedInReturnPosition;
10741079
container = saveContainer;
10751080
thisParentContainer = saveThisParentContainer;
10761081
blockScopeContainer = savedBlockScopeContainer;
@@ -1571,7 +1576,10 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
15711576
}
15721577

15731578
function bindReturnOrThrow(node: ReturnStatement | ThrowStatement): void {
1579+
const savedInReturnPosition = inReturnPosition;
1580+
inReturnPosition = true;
15741581
bind(node.expression);
1582+
inReturnPosition = savedInReturnPosition;
15751583
if (node.kind === SyntaxKind.ReturnStatement) {
15761584
hasExplicitReturn = true;
15771585
if (currentReturnTarget) {
@@ -2016,10 +2024,16 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
20162024
hasFlowEffects = false;
20172025
bindCondition(node.condition, trueLabel, falseLabel);
20182026
currentFlow = finishFlowLabel(trueLabel);
2027+
if (inReturnPosition) {
2028+
node.flowNodeWhenTrue = currentFlow;
2029+
}
20192030
bind(node.questionToken);
20202031
bind(node.whenTrue);
20212032
addAntecedent(postExpressionLabel, currentFlow);
20222033
currentFlow = finishFlowLabel(falseLabel);
2034+
if (inReturnPosition) {
2035+
node.flowNodeWhenFalse = currentFlow;
2036+
}
20232037
bind(node.colonToken);
20242038
bind(node.whenFalse);
20252039
addAntecedent(postExpressionLabel, currentFlow);

src/compiler/checker.ts

+409-40
Large diffs are not rendered by default.

src/compiler/factory/nodeFactory.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3481,6 +3481,8 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
34813481
propagateChildFlags(node.whenTrue) |
34823482
propagateChildFlags(node.colonToken) |
34833483
propagateChildFlags(node.whenFalse);
3484+
node.flowNodeWhenFalse = undefined;
3485+
node.flowNodeWhenTrue = undefined;
34843486
return node;
34853487
}
34863488

src/compiler/types.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -2735,6 +2735,10 @@ export interface ConditionalExpression extends Expression {
27352735
readonly whenTrue: Expression;
27362736
readonly colonToken: ColonToken;
27372737
readonly whenFalse: Expression;
2738+
/** @internal*/
2739+
flowNodeWhenTrue: FlowNode | undefined;
2740+
/** @internal */
2741+
flowNodeWhenFalse: FlowNode | undefined;
27382742
}
27392743

27402744
export type FunctionBody = Block;
@@ -6240,6 +6244,7 @@ export interface NodeLinks {
62406244
decoratorSignature?: Signature; // Signature for decorator as if invoked by the runtime.
62416245
spreadIndices?: { first: number | undefined, last: number | undefined }; // Indices of first and last spread elements in array literal
62426246
parameterInitializerContainsUndefined?: boolean; // True if this is a parameter declaration whose type annotation contains "undefined".
6247+
contextualReturnType?: Type; // If the node is a return statement's expression, then this is the contextual return type.
62436248
fakeScopeForSignatureDeclaration?: "params" | "typeParams"; // If present, this is a fake scope injected into an enclosing declaration chain.
62446249
assertionExpressionType?: Type; // Cached type of the expression of a type assertion
62456250
potentialThisCollisions?: Node[];
@@ -6506,6 +6511,8 @@ export const enum ObjectFlags {
65066511
IsGenericIndexType = 1 << 23, // Union or intersection contains generic index type
65076512
/** @internal */
65086513
IsGenericType = IsGenericObjectType | IsGenericIndexType,
6514+
/** @internal */
6515+
IsNarrowingType = 1 << 24, // Substitution type that comes from type narrowing
65096516

65106517
// Flags that require TypeFlags.Union
65116518
/** @internal */
@@ -6905,12 +6912,16 @@ export interface StringMappingType extends InstantiableType {
69056912
}
69066913

69076914
// Type parameter substitution (TypeFlags.Substitution)
6908-
// Substitution types are created for type parameters or indexed access types that occur in the
6915+
// - Substitution types are created for type parameters or indexed access types that occur in the
69096916
// true branch of a conditional type. For example, in 'T extends string ? Foo<T> : Bar<T>', the
69106917
// reference to T in Foo<T> is resolved as a substitution type that substitutes 'string & T' for T.
69116918
// Thus, if Foo has a 'string' constraint on its type parameter, T will satisfy it.
6912-
// Substitution type are also created for NoInfer<T> types. Those are represented as substitution
6919+
// - Substitution types are also created for NoInfer<T> types. Those are represented as substitution
69136920
// types where the constraint is type 'unknown' (which is never generated for the case above).
6921+
// - Substitution types are also created for return type narrowing:
6922+
// if a type parameter `T` is linked to a parameter `x` and `x`'s narrowed type is `S`,
6923+
// we represent that with a substitution type with base `T` and constraint `S`.
6924+
// The resulting substitution type has `ObjectFlags.IsNarrowedType` set.
69146925
export interface SubstitutionType extends InstantiableType {
69156926
objectFlags: ObjectFlags;
69166927
baseType: Type; // Target type
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//// [tests/cases/compiler/arrowExpressionJs.ts] ////
2+
3+
=== mytest.js ===
4+
/**
5+
* @template T
6+
* @param {T|undefined} value value or not
7+
* @returns {T} result value
8+
*/
9+
const cloneObjectGood = value => /** @type {T} */({ ...value });
10+
>cloneObjectGood : Symbol(cloneObjectGood, Decl(mytest.js, 5, 5))
11+
>value : Symbol(value, Decl(mytest.js, 5, 23))
12+
>value : Symbol(value, Decl(mytest.js, 5, 23))
13+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//// [tests/cases/compiler/arrowExpressionJs.ts] ////
2+
3+
=== mytest.js ===
4+
/**
5+
* @template T
6+
* @param {T|undefined} value value or not
7+
* @returns {T} result value
8+
*/
9+
const cloneObjectGood = value => /** @type {T} */({ ...value });
10+
>cloneObjectGood : <T>(value: T | undefined) => T
11+
> : ^ ^^ ^^ ^^^^^
12+
>value => /** @type {T} */({ ...value }) : <T>(value: T | undefined) => T
13+
> : ^ ^^ ^^ ^^^^^
14+
>value : T | undefined
15+
> : ^^^^^^^^^^^^^
16+
>({ ...value }) : T
17+
> : ^
18+
>{ ...value } : {}
19+
> : ^^
20+
>value : T | undefined
21+
> : ^^^^^^^^^^^^^
22+

0 commit comments

Comments
 (0)