diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 5a6edd9433c..25ee03c67f7 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3021,6 +3021,10 @@ namespace ts { return strictNullChecks && optional ? includeFalsyTypes(type, TypeFlags.Undefined) : type; } + function isAutoTypeInitializer(node: Expression) { + return !node || node.kind === SyntaxKind.ObjectLiteralExpression && (node).properties.length === 0; + } + // Return the inferred type for a variable, parameter, or property declaration function getTypeForVariableLikeDeclaration(declaration: VariableLikeDeclaration, includeOptionality: boolean): Type { if (declaration.flags & NodeFlags.JavaScriptFile) { @@ -3056,8 +3060,9 @@ namespace ts { } // Use control flow type inference for non-ambient, non-exported var or let variables with no initializer - if (declaration.kind === SyntaxKind.VariableDeclaration && !isBindingPattern(declaration.name) && !declaration.initializer && - !(getCombinedNodeFlags(declaration) & (NodeFlags.Export | NodeFlags.Const)) && !isInAmbientContext(declaration)) { + if (declaration.kind === SyntaxKind.VariableDeclaration && !isBindingPattern(declaration.name) && + !(getCombinedNodeFlags(declaration) & (NodeFlags.Export | NodeFlags.Const)) && + isAutoTypeInitializer(declaration.initializer) && !isInAmbientContext(declaration)) { return autoType; } @@ -3230,7 +3235,8 @@ namespace ts { } const declaration = symbol.valueDeclaration; if (!declaration && symbol.openType) { - return getFlowTypeOfReference(symbol.propAccess, autoType, /*assumeInitialized*/ false, symbol.openType.flowNode, symbol.openType.flowContainer); + return getWidenedType(getFlowTypeOfReference(symbol.propAccess, autoType, /*assumeInitialized*/ false, + symbol.openType.flowNode, symbol.openType.flowContainer)); } // Handle catch clause variables if (declaration.parent.kind === SyntaxKind.CatchClause) { @@ -4255,7 +4261,6 @@ namespace ts { } if (type.assignedMembers) { setObjectTypeMembers(type, type.assignedMembers, emptyArray, emptyArray, undefined, undefined); - type.assignedMembers = undefined; return; } const symbol = type.symbol; @@ -8221,7 +8226,7 @@ namespace ts { } function isOpenType(type: Type): boolean { - return !!(type.flags & TypeFlags.Anonymous && (type).assignedMembers); + return !!(type.flags & TypeFlags.Anonymous && (type).assignedMembers) && !(type).members; } function expandOpenType(type: AnonymousType, propAccess: PropertyAccessExpression) { @@ -8239,7 +8244,7 @@ namespace ts { return declaredType; } const initialType = assumeInitialized ? declaredType : - declaredType === autoType ? undefinedType : + declaredType === autoType ? reference.kind === SyntaxKind.PropertyAccessExpression ? neverType : undefinedType : includeFalsyTypes(declaredType, TypeFlags.Undefined); const visitedFlowStart = visitedFlowCount; const result = getTypeFromFlowType(getTypeAtFlowNode(flowNode)); @@ -8248,9 +8253,8 @@ namespace ts { return declaredType; } if (isOpenType(result)) { - if (reference.parent.kind === SyntaxKind.PropertyAccessExpression && isAssignmentTarget(reference.parent)) { - return anyType; - } + // Resolving the members of the open type has the effect of sealing it from further expansion. + resolveStructuredTypeMembers(result); } return result; @@ -8325,11 +8329,22 @@ namespace ts { declaredType.flags & TypeFlags.Union ? getAssignmentReducedType(declaredType, type) : declaredType; } - // See if we're expanding an open type. - if (declaredType === autoType && node.kind === SyntaxKind.PropertyAccessExpression && isMatchingReference(reference, (node).expression)) { - const type = getTypeFromFlowType(getTypeAtFlowNode(flow.antecedent)); - if (isOpenType(type)) { - return expandOpenType(type, node); + // When the declared type is autoType, we're analyzing a control flow typed local variable or + // a property of an open type. + if (declaredType === autoType) { + // Check if we're expanding an open type. Specifically, if the node is an assignment to a + // property of the reference we're analyzing and if the current control flow type of the + // reference is an open type, we add a property to that open type. + if (node.kind === SyntaxKind.PropertyAccessExpression && isMatchingReference(reference, (node).expression)) { + const type = getTypeFromFlowType(getTypeAtFlowNode(flow.antecedent)); + if (isOpenType(type)) { + return expandOpenType(type, node); + } + } + // Check if we're implicitly initializing a property to undefined. Specifically, for a reference + // 'x.y.z', if we're assigning a value to 'x.y' we consider 'z' to be undefined. + if (reference.kind === SyntaxKind.PropertyAccessExpression && isMatchingReference((reference).expression, node)) { + return undefinedType; } } // We didn't have a direct match. However, if the reference is a dotted name, this @@ -10789,6 +10804,15 @@ namespace ts { return type; } + // When a property access expression 'x.y' is the target of an assignment and when 'x' has an + // open type, if the flow node associated with the open type is the same as the flow node associated + // with the property access (i.e. if this is the specific open type that was constructed for 'x' in + // the property access), then we consider 'x.y' to have type any so that any assignment is permitted. + if (type.flags & TypeFlags.Anonymous && (type).assignedMembers && isAssignmentTarget(node) && + node.flowNode === (type).flowNode) { + return anyType; + } + const apparentType = getApparentType(getWidenedType(type)); if (apparentType === unknownType || (type.flags & TypeFlags.TypeParameter && isTypeAny(apparentType))) { // handle cases when type is Type parameter with invalid or any constraint