Skip to content

Commit 2d0a00d

Browse files
authored
Fix destructuring and narrowing interaction (#47337)
* WIP: pass in checkmode to getNarrowableTypeForReference * add tests * another pass through new check mode argument * rename new check mode * only use rest flag for rest elements in objects * add and update tests * change check mode flag name * restore package-lock.json * fix comments * get rid of fourslash tests * fix caching in checkExpressionCached when checkMode is not normal * Don't distinguish between object and array rest elements * get rid of undefined check mode * don't make includeOptionality into checkmode flag
1 parent d5c3015 commit 2d0a00d

7 files changed

+480
-32
lines changed

src/compiler/checker.ts

+52-30
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,15 @@ namespace ts {
175175
}
176176

177177
const enum CheckMode {
178-
Normal = 0, // Normal type checking
179-
Contextual = 1 << 0, // Explicitly assigned contextual type, therefore not cacheable
180-
Inferential = 1 << 1, // Inferential typing
181-
SkipContextSensitive = 1 << 2, // Skip context sensitive function expressions
182-
SkipGenericFunctions = 1 << 3, // Skip single signature generic functions
183-
IsForSignatureHelp = 1 << 4, // Call resolution for purposes of signature help
178+
Normal = 0, // Normal type checking
179+
Contextual = 1 << 0, // Explicitly assigned contextual type, therefore not cacheable
180+
Inferential = 1 << 1, // Inferential typing
181+
SkipContextSensitive = 1 << 2, // Skip context sensitive function expressions
182+
SkipGenericFunctions = 1 << 3, // Skip single signature generic functions
183+
IsForSignatureHelp = 1 << 4, // Call resolution for purposes of signature help
184+
RestBindingElement = 1 << 5, // Checking a type that is going to be used to determine the type of a rest binding element
185+
// e.g. in `const { a, ...rest } = foo`, when checking the type of `foo` to determine the type of `rest`,
186+
// we need to preserve generic types instead of substituting them for constraints
184187
}
185188

186189
const enum SignatureCheckMode {
@@ -8461,9 +8464,12 @@ namespace ts {
84618464

84628465
// Return the type of a binding element parent. We check SymbolLinks first to see if a type has been
84638466
// assigned by contextual typing.
8464-
function getTypeForBindingElementParent(node: BindingElementGrandparent) {
8467+
function getTypeForBindingElementParent(node: BindingElementGrandparent, checkMode: CheckMode) {
8468+
if (checkMode !== CheckMode.Normal) {
8469+
return getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ false, checkMode);
8470+
}
84658471
const symbol = getSymbolOfNode(node);
8466-
return symbol && getSymbolLinks(symbol).type || getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ false);
8472+
return symbol && getSymbolLinks(symbol).type || getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ false, checkMode);
84678473
}
84688474

84698475
function getRestType(source: Type, properties: PropertyName[], symbol: Symbol | undefined): Type {
@@ -8594,7 +8600,8 @@ namespace ts {
85948600

85958601
/** Return the inferred type for a binding element */
85968602
function getTypeForBindingElement(declaration: BindingElement): Type | undefined {
8597-
const parentType = getTypeForBindingElementParent(declaration.parent.parent);
8603+
const checkMode = declaration.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal;
8604+
const parentType = getTypeForBindingElementParent(declaration.parent.parent, checkMode);
85988605
return parentType && getBindingElementTypeFromParentType(declaration, parentType);
85998606
}
86008607

@@ -8667,9 +8674,9 @@ namespace ts {
86678674
if (getEffectiveTypeAnnotationNode(walkUpBindingElementsAndPatterns(declaration))) {
86688675
// In strict null checking mode, if a default value of a non-undefined type is specified, remove
86698676
// undefined from the final type.
8670-
return strictNullChecks && !(getFalsyFlags(checkDeclarationInitializer(declaration)) & TypeFlags.Undefined) ? getNonUndefinedType(type) : type;
8677+
return strictNullChecks && !(getFalsyFlags(checkDeclarationInitializer(declaration, CheckMode.Normal)) & TypeFlags.Undefined) ? getNonUndefinedType(type) : type;
86718678
}
8672-
return widenTypeInferredFromInitializer(declaration, getUnionType([getNonUndefinedType(type), checkDeclarationInitializer(declaration)], UnionReduction.Subtype));
8679+
return widenTypeInferredFromInitializer(declaration, getUnionType([getNonUndefinedType(type), checkDeclarationInitializer(declaration, CheckMode.Normal)], UnionReduction.Subtype));
86738680
}
86748681

86758682
function getTypeForDeclarationFromJSDocComment(declaration: Node) {
@@ -8695,11 +8702,15 @@ namespace ts {
86958702
}
86968703

86978704
// Return the inferred type for a variable, parameter, or property declaration
8698-
function getTypeForVariableLikeDeclaration(declaration: ParameterDeclaration | PropertyDeclaration | PropertySignature | VariableDeclaration | BindingElement | JSDocPropertyLikeTag, includeOptionality: boolean): Type | undefined {
8705+
function getTypeForVariableLikeDeclaration(
8706+
declaration: ParameterDeclaration | PropertyDeclaration | PropertySignature | VariableDeclaration | BindingElement | JSDocPropertyLikeTag,
8707+
includeOptionality: boolean,
8708+
checkMode: CheckMode,
8709+
): Type | undefined {
86998710
// A variable declared in a for..in statement is of type string, or of type keyof T when the
87008711
// right hand expression is of a type parameter type.
87018712
if (isVariableDeclaration(declaration) && declaration.parent.parent.kind === SyntaxKind.ForInStatement) {
8702-
const indexType = getIndexType(getNonNullableTypeIfNeeded(checkExpression(declaration.parent.parent.expression)));
8713+
const indexType = getIndexType(getNonNullableTypeIfNeeded(checkExpression(declaration.parent.parent.expression, /*checkMode*/ checkMode)));
87038714
return indexType.flags & (TypeFlags.TypeParameter | TypeFlags.Index) ? getExtractStringType(indexType) : stringType;
87048715
}
87058716

@@ -8784,7 +8795,7 @@ namespace ts {
87848795
return containerObjectType;
87858796
}
87868797
}
8787-
const type = widenTypeInferredFromInitializer(declaration, checkDeclarationInitializer(declaration));
8798+
const type = widenTypeInferredFromInitializer(declaration, checkDeclarationInitializer(declaration, checkMode));
87888799
return addOptionality(type, isProperty, isOptional);
87898800
}
87908801

@@ -9177,7 +9188,7 @@ namespace ts {
91779188
// contextual type or, if the element itself is a binding pattern, with the type implied by that binding
91789189
// pattern.
91799190
const contextualType = isBindingPattern(element.name) ? getTypeFromBindingPattern(element.name, /*includePatternInType*/ true, /*reportErrors*/ false) : unknownType;
9180-
return addOptionality(widenTypeInferredFromInitializer(element, checkDeclarationInitializer(element, contextualType)));
9191+
return addOptionality(widenTypeInferredFromInitializer(element, checkDeclarationInitializer(element, CheckMode.Normal, contextualType)));
91819192
}
91829193
if (isBindingPattern(element.name)) {
91839194
return getTypeFromBindingPattern(element.name, includePatternInType, reportErrors);
@@ -9269,7 +9280,7 @@ namespace ts {
92699280
// binding pattern [x, s = ""]. Because the contextual type is a tuple type, the resulting type of [1, "one"] is the
92709281
// tuple type [number, string]. Thus, the type inferred for 'x' is number and the type inferred for 's' is string.
92719282
function getWidenedTypeForVariableLikeDeclaration(declaration: ParameterDeclaration | PropertyDeclaration | PropertySignature | VariableDeclaration | BindingElement | JSDocPropertyLikeTag, reportErrors?: boolean): Type {
9272-
return widenTypeForVariableLikeDeclaration(getTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ true), declaration, reportErrors);
9283+
return widenTypeForVariableLikeDeclaration(getTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ true, CheckMode.Normal), declaration, reportErrors);
92739284
}
92749285

92759286
function isGlobalSymbolConstructor(node: Node) {
@@ -25019,12 +25030,16 @@ namespace ts {
2501925030
return !!(type.flags & TypeFlags.Instantiable && !maybeTypeOfKind(getBaseConstraintOrType(type), TypeFlags.Nullable));
2502025031
}
2502125032

25022-
function hasNonBindingPatternContextualTypeWithNoGenericTypes(node: Node) {
25033+
function hasContextualTypeWithNoGenericTypes(node: Node, checkMode: CheckMode | undefined) {
2502325034
// Computing the contextual type for a child of a JSX element involves resolving the type of the
2502425035
// element's tag name, so we exclude that here to avoid circularities.
25036+
// If check mode has `CheckMode.RestBindingElement`, we skip binding pattern contextual types,
25037+
// as we want the type of a rest element to be generic when possible.
2502525038
const contextualType = (isIdentifier(node) || isPropertyAccessExpression(node) || isElementAccessExpression(node)) &&
2502625039
!((isJsxOpeningElement(node.parent) || isJsxSelfClosingElement(node.parent)) && node.parent.tagName === node) &&
25027-
getContextualType(node, ContextFlags.SkipBindingPatterns);
25040+
(checkMode && checkMode & CheckMode.RestBindingElement ?
25041+
getContextualType(node, ContextFlags.SkipBindingPatterns)
25042+
: getContextualType(node));
2502825043
return contextualType && !isGenericType(contextualType);
2502925044
}
2503025045

@@ -25038,7 +25053,7 @@ namespace ts {
2503825053
// 'string | undefined' to give control flow analysis the opportunity to narrow to type 'string'.
2503925054
const substituteConstraints = !(checkMode && checkMode & CheckMode.Inferential) &&
2504025055
someType(type, isGenericTypeWithUnionConstraint) &&
25041-
(isConstraintPosition(type, reference) || hasNonBindingPatternContextualTypeWithNoGenericTypes(reference));
25056+
(isConstraintPosition(type, reference) || hasContextualTypeWithNoGenericTypes(reference, checkMode));
2504225057
return substituteConstraints ? mapType(type, t => t.flags & TypeFlags.Instantiable && !isMappedTypeGenericIndexedAccess(t) ? getBaseConstraintOrType(t) : t) : type;
2504325058
}
2504425059

@@ -25110,7 +25125,7 @@ namespace ts {
2511025125
const links = getNodeLinks(location);
2511125126
if (!(links.flags & NodeCheckFlags.InCheckIdentifier)) {
2511225127
links.flags |= NodeCheckFlags.InCheckIdentifier;
25113-
const parentType = getTypeForBindingElementParent(parent);
25128+
const parentType = getTypeForBindingElementParent(parent, CheckMode.Normal);
2511425129
links.flags &= ~NodeCheckFlags.InCheckIdentifier;
2511525130
if (parentType && parentType.flags & TypeFlags.Union && !(parent.kind === SyntaxKind.Parameter && isSymbolAssigned(symbol))) {
2511625131
const pattern = declaration.parent;
@@ -26047,7 +26062,7 @@ namespace ts {
2604726062
const parent = declaration.parent.parent;
2604826063
const name = declaration.propertyName || declaration.name;
2604926064
const parentType = getContextualTypeForVariableLikeDeclaration(parent) ||
26050-
parent.kind !== SyntaxKind.BindingElement && parent.initializer && checkDeclarationInitializer(parent);
26065+
parent.kind !== SyntaxKind.BindingElement && parent.initializer && checkDeclarationInitializer(parent, declaration.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal);
2605126066
if (!parentType || isBindingPattern(name) || isComputedNonLiteralName(name)) return undefined;
2605226067
if (parent.name.kind === SyntaxKind.ArrayBindingPattern) {
2605326068
const index = indexOfNode(declaration.parent.elements, declaration);
@@ -31807,7 +31822,7 @@ namespace ts {
3180731822
const links = getSymbolLinks(parameter);
3180831823
if (!links.type) {
3180931824
const declaration = parameter.valueDeclaration as ParameterDeclaration;
31810-
links.type = type || getWidenedTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ true);
31825+
links.type = type || getWidenedTypeForVariableLikeDeclaration(declaration, /*reportErrors*/ true);
3181131826
if (declaration.name.kind !== SyntaxKind.Identifier) {
3181231827
// if inference didn't come up with anything but unknown, fall back to the binding pattern if present.
3181331828
if (links.type === unknownType) {
@@ -33724,11 +33739,11 @@ namespace ts {
3372433739
}
3372533740

3372633741
function checkExpressionCached(node: Expression | QualifiedName, checkMode?: CheckMode): Type {
33742+
if (checkMode && checkMode !== CheckMode.Normal) {
33743+
return checkExpression(node, checkMode);
33744+
}
3372733745
const links = getNodeLinks(node);
3372833746
if (!links.resolvedType) {
33729-
if (checkMode && checkMode !== CheckMode.Normal) {
33730-
return checkExpression(node, checkMode);
33731-
}
3373233747
// When computing a type that we're going to cache, we need to ignore any ongoing control flow
3373333748
// analysis because variables may have transient types in indeterminable states. Moving flowLoopStart
3373433749
// to the top of the stack ensures all transient types are computed from a known point.
@@ -33750,10 +33765,16 @@ namespace ts {
3375033765
isJSDocTypeAssertion(node);
3375133766
}
3375233767

33753-
function checkDeclarationInitializer(declaration: HasExpressionInitializer, contextualType?: Type | undefined) {
33768+
function checkDeclarationInitializer(
33769+
declaration: HasExpressionInitializer,
33770+
checkMode: CheckMode,
33771+
contextualType?: Type | undefined
33772+
) {
3375433773
const initializer = getEffectiveInitializer(declaration)!;
3375533774
const type = getQuickTypeOfExpression(initializer) ||
33756-
(contextualType ? checkExpressionWithContextualType(initializer, contextualType, /*inferenceContext*/ undefined, CheckMode.Normal) : checkExpressionCached(initializer));
33775+
(contextualType ?
33776+
checkExpressionWithContextualType(initializer, contextualType, /*inferenceContext*/ undefined, checkMode || CheckMode.Normal)
33777+
: checkExpressionCached(initializer, checkMode));
3375733778
return isParameter(declaration) && declaration.name.kind === SyntaxKind.ArrayBindingPattern &&
3375833779
isTupleType(type) && !type.target.hasRestElement && getTypeReferenceArity(type) < declaration.name.elements.length ?
3375933780
padTupleType(type, declaration.name) : type;
@@ -37011,7 +37032,8 @@ namespace ts {
3701137032

3701237033
// check private/protected variable access
3701337034
const parent = node.parent.parent;
37014-
const parentType = getTypeForBindingElementParent(parent);
37035+
const parentCheckMode = node.dotDotDotToken ? CheckMode.RestBindingElement : CheckMode.Normal;
37036+
const parentType = getTypeForBindingElementParent(parent, parentCheckMode);
3701537037
const name = node.propertyName || node.name;
3701637038
if (parentType && !isBindingPattern(name)) {
3701737039
const exprType = getLiteralTypeFromPropertyName(name);
@@ -38409,7 +38431,7 @@ namespace ts {
3840938431
const declaration = catchClause.variableDeclaration;
3841038432
const typeNode = getEffectiveTypeAnnotationNode(getRootDeclaration(declaration));
3841138433
if (typeNode) {
38412-
const type = getTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ false);
38434+
const type = getTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ false, CheckMode.Normal);
3841338435
if (type && !(type.flags & TypeFlags.AnyOrUnknown)) {
3841438436
grammarErrorOnFirstToken(typeNode, Diagnostics.Catch_clause_variable_type_annotation_must_be_any_or_unknown_if_specified);
3841538437
}
@@ -41440,7 +41462,7 @@ namespace ts {
4144041462
}
4144141463

4144241464
if (isBindingPattern(node)) {
41443-
return getTypeForVariableLikeDeclaration(node.parent, /*includeOptionality*/ true) || errorType;
41465+
return getTypeForVariableLikeDeclaration(node.parent, /*includeOptionality*/ true, CheckMode.Normal) || errorType;
4144441466
}
4144541467

4144641468
if (isInRightSideOfImportOrExportAssignment(node as Identifier)) {

tests/baselines/reference/genericObjectSpreadResultInSwitch.types

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const getType = <P extends Params>(params: P) => {
2525
>rest : Omit<P, "foo">
2626

2727
} = params;
28-
>params : P
28+
>params : Params
2929

3030
return rest;
3131
>rest : Omit<P, "foo">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//// [narrowingDestructuring.ts]
2+
type X = { kind: "a", a: string } | { kind: "b", b: string }
3+
4+
function func<T extends X>(value: T) {
5+
if (value.kind === "a") {
6+
value.a;
7+
const { a } = value;
8+
} else {
9+
value.b;
10+
const { b } = value;
11+
}
12+
}
13+
14+
type Z = { kind: "f", f: { a: number, b: string, c: number } }
15+
| { kind: "g", g: { a: string, b: number, c: string }};
16+
17+
function func2<T extends Z>(value: T) {
18+
if (value.kind === "f") {
19+
const { f: f1 } = value;
20+
const { f: { a, ...spread } } = value;
21+
value.f;
22+
} else {
23+
const { g: { c, ...spread } } = value;
24+
value.g;
25+
}
26+
}
27+
28+
function func3<T extends { kind: "a", a: string } | { kind: "b", b: number }>(t: T) {
29+
if (t.kind === "a") {
30+
const { kind, ...r1 } = t;
31+
const r2 = (({ kind, ...rest }) => rest)(t);
32+
}
33+
}
34+
35+
function farr<T extends [number, string, string] | [string, number, number]>(x: T) {
36+
const [head, ...tail] = x;
37+
if (x[0] === 'number') {
38+
const [head, ...tail] = x;
39+
}
40+
}
41+
42+
//// [narrowingDestructuring.js]
43+
var __rest = (this && this.__rest) || function (s, e) {
44+
var t = {};
45+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
46+
t[p] = s[p];
47+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
48+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
49+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
50+
t[p[i]] = s[p[i]];
51+
}
52+
return t;
53+
};
54+
function func(value) {
55+
if (value.kind === "a") {
56+
value.a;
57+
var a = value.a;
58+
}
59+
else {
60+
value.b;
61+
var b = value.b;
62+
}
63+
}
64+
function func2(value) {
65+
if (value.kind === "f") {
66+
var f1 = value.f;
67+
var _a = value.f, a = _a.a, spread = __rest(_a, ["a"]);
68+
value.f;
69+
}
70+
else {
71+
var _b = value.g, c = _b.c, spread = __rest(_b, ["c"]);
72+
value.g;
73+
}
74+
}
75+
function func3(t) {
76+
if (t.kind === "a") {
77+
var kind = t.kind, r1 = __rest(t, ["kind"]);
78+
var r2 = (function (_a) {
79+
var kind = _a.kind, rest = __rest(_a, ["kind"]);
80+
return rest;
81+
})(t);
82+
}
83+
}
84+
function farr(x) {
85+
var head = x[0], tail = x.slice(1);
86+
if (x[0] === 'number') {
87+
var head_1 = x[0], tail_1 = x.slice(1);
88+
}
89+
}

0 commit comments

Comments
 (0)