diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 7f45e4091b7..51550ef873f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -132,6 +132,7 @@ namespace ts { const esSymbolType = createIntrinsicType(TypeFlags.ESSymbol, "symbol"); const voidType = createIntrinsicType(TypeFlags.Void, "void"); const neverType = createIntrinsicType(TypeFlags.Never, "never"); + const silentNeverType = createIntrinsicType(TypeFlags.Never, "never"); const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); const emptyGenericType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); @@ -147,6 +148,7 @@ namespace ts { const anySignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false); const unknownSignature = createSignature(undefined, undefined, undefined, emptyArray, unknownType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false); const resolvingSignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false); + const silentNeverSignature = createSignature(undefined, undefined, undefined, emptyArray, silentNeverType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false); const enumNumberIndexInfo = createIndexInfo(stringType, /*isReadonly*/ true); @@ -8073,8 +8075,11 @@ namespace ts { // we remove type string. function getAssignmentReducedType(declaredType: UnionType, assignedType: Type) { if (declaredType !== assignedType) { + if (assignedType.flags & TypeFlags.Never) { + return assignedType; + } const reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t)); - if (reducedType !== neverType) { + if (!(reducedType.flags & TypeFlags.Never)) { return reducedType; } } @@ -8354,7 +8359,7 @@ namespace ts { const visitedFlowStart = visitedFlowCount; const result = getTypeFromFlowType(getTypeAtFlowNode(reference.flowNode)); visitedFlowCount = visitedFlowStart; - if (reference.parent.kind === SyntaxKind.NonNullExpression && getTypeWithFacts(result, TypeFacts.NEUndefinedOrNull) === neverType) { + if (reference.parent.kind === SyntaxKind.NonNullExpression && getTypeWithFacts(result, TypeFacts.NEUndefinedOrNull).flags & TypeFlags.Never) { return declaredType; } return result; @@ -8443,17 +8448,18 @@ namespace ts { function getTypeAtFlowCondition(flow: FlowCondition): FlowType { const flowType = getTypeAtFlowNode(flow.antecedent); let type = getTypeFromFlowType(flowType); - if (type !== neverType) { + if (!(type.flags & TypeFlags.Never)) { // If we have an antecedent type (meaning we're reachable in some way), we first // attempt to narrow the antecedent type. If that produces the never type, and if // the antecedent type is incomplete (i.e. a transient type in a loop), then we // take the type guard as an indication that control *could* reach here once we - // have the complete type. We proceed by reverting to the declared type and then - // narrow that. + // have the complete type. We proceed by switching to the silent never type which + // doesn't report errors when operators are applied to it. Note that this is the + // *only* place a silent never type is ever generated. const assumeTrue = (flow.flags & FlowFlags.TrueCondition) !== 0; type = narrowType(type, flow.expression, assumeTrue); - if (type === neverType && isIncomplete(flowType)) { - type = narrowType(declaredType, flow.expression, assumeTrue); + if (type.flags & TypeFlags.Never && isIncomplete(flowType)) { + type = silentNeverType; } } return createFlowType(type, isIncomplete(flowType)); @@ -8662,7 +8668,7 @@ namespace ts { } if (assumeTrue) { const narrowedType = filterType(type, t => areTypesComparable(t, valueType)); - return narrowedType !== neverType ? narrowedType : type; + return narrowedType.flags & TypeFlags.Never ? type : narrowedType; } return isUnitType(valueType) ? filterType(type, t => t !== valueType) : type; } @@ -8705,12 +8711,12 @@ namespace ts { const clauseTypes = switchTypes.slice(clauseStart, clauseEnd); const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, neverType); const discriminantType = getUnionType(clauseTypes); - const caseType = discriminantType === neverType ? neverType : filterType(type, t => isTypeComparableTo(discriminantType, t)); + const caseType = discriminantType.flags & TypeFlags.Never ? neverType : filterType(type, t => isTypeComparableTo(discriminantType, t)); if (!hasDefaultClause) { return caseType; } const defaultType = filterType(type, t => !(isUnitType(t) && contains(switchTypes, t))); - return caseType === neverType ? defaultType : getUnionType([caseType, defaultType]); + return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]); } function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { @@ -8774,7 +8780,7 @@ namespace ts { // the candidate type. If one or more constituents remain, return a union of those. if (type.flags & TypeFlags.Union) { const assignableType = filterType(type, t => isTypeInstanceOf(t, candidate)); - if (assignableType !== neverType) { + if (!(assignableType.flags & TypeFlags.Never)) { return assignableType; } } @@ -10892,7 +10898,7 @@ namespace ts { function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier) { const type = checkNonNullExpression(left); - if (isTypeAny(type)) { + if (isTypeAny(type) || type === silentNeverType) { return type; } @@ -11039,8 +11045,8 @@ namespace ts { const objectType = getApparentType(checkNonNullExpression(node.expression)); const indexType = node.argumentExpression ? checkExpression(node.argumentExpression) : unknownType; - if (objectType === unknownType) { - return unknownType; + if (objectType === unknownType || objectType === silentNeverType) { + return objectType; } const isConstEnum = isConstEnumObjectType(objectType); @@ -12090,6 +12096,9 @@ namespace ts { } const funcType = checkNonNullExpression(node.expression); + if (funcType === silentNeverType) { + return silentNeverSignature; + } const apparentType = getApparentType(funcType); if (apparentType === unknownType) { @@ -12162,6 +12171,9 @@ namespace ts { } let expressionType = checkNonNullExpression(node.expression); + if (expressionType === silentNeverType) { + return silentNeverSignature; + } // If expressionType's apparent type(section 3.8.1) is an object type with one or // more construct signatures, the expression is processed in the same manner as a @@ -12721,7 +12733,7 @@ namespace ts { // the native Promise type by the caller. type = checkAwaitedType(type, func, Diagnostics.Return_expression_in_async_function_does_not_have_a_valid_callable_then_member); } - if (type === neverType) { + if (type.flags & TypeFlags.Never) { hasReturnOfTypeNever = true; } else if (!contains(aggregatedTypes, type)) { @@ -12771,7 +12783,7 @@ namespace ts { const hasExplicitReturn = func.flags & NodeFlags.HasExplicitReturn; - if (returnType === neverType) { + if (returnType && returnType.flags & TypeFlags.Never) { error(func.type, Diagnostics.A_function_returning_never_cannot_have_a_reachable_end_point); } else if (returnType && !hasExplicitReturn) { @@ -13027,6 +13039,9 @@ namespace ts { function checkPrefixUnaryExpression(node: PrefixUnaryExpression): Type { const operandType = checkExpression(node.operand); + if (operandType === silentNeverType) { + return silentNeverType; + } if (node.operator === SyntaxKind.MinusToken && node.operand.kind === SyntaxKind.NumericLiteral) { return getLiteralTypeForText(TypeFlags.NumberLiteral, "" + -(node.operand).text); } @@ -13060,6 +13075,9 @@ namespace ts { function checkPostfixUnaryExpression(node: PostfixUnaryExpression): Type { const operandType = checkExpression(node.operand); + if (operandType === silentNeverType) { + return silentNeverType; + } const ok = checkArithmeticOperandType(node.operand, getNonNullableType(operandType), Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_or_an_enum_type); if (ok) { @@ -13124,6 +13142,9 @@ namespace ts { } function checkInstanceOfExpression(left: Expression, right: Expression, leftType: Type, rightType: Type): Type { + if (leftType === silentNeverType || rightType === silentNeverType) { + return silentNeverType; + } // TypeScript 1.0 spec (April 2014): 4.15.4 // The instanceof operator requires the left operand to be of type Any, an object type, or a type parameter type, // and the right operand to be of type Any or a subtype of the 'Function' interface type. @@ -13140,6 +13161,9 @@ namespace ts { } function checkInExpression(left: Expression, right: Expression, leftType: Type, rightType: Type): Type { + if (leftType === silentNeverType || rightType === silentNeverType) { + return silentNeverType; + } // TypeScript 1.0 spec (April 2014): 4.15.5 // The in operator requires the left operand to be of type Any, the String primitive type, or the Number primitive type, // and the right operand to be of type Any, an object type, or a type parameter type. @@ -13404,6 +13428,9 @@ namespace ts { case SyntaxKind.CaretEqualsToken: case SyntaxKind.AmpersandToken: case SyntaxKind.AmpersandEqualsToken: + if (leftType === silentNeverType || rightType === silentNeverType) { + return silentNeverType; + } // TypeScript 1.0 spec (April 2014): 4.19.1 // These operators require their operands to be of type Any, the Number primitive type, // or an enum type. Operands of an enum type are treated @@ -13436,6 +13463,9 @@ namespace ts { return numberType; case SyntaxKind.PlusToken: case SyntaxKind.PlusEqualsToken: + if (leftType === silentNeverType || rightType === silentNeverType) { + return silentNeverType; + } // TypeScript 1.0 spec (April 2014): 4.19.2 // The binary + operator requires both operands to be of the Number primitive type or an enum type, // or at least one of the operands to be of type Any or the String primitive type. @@ -16268,7 +16298,7 @@ namespace ts { // Now that we've removed all the StringLike types, if no constituents remain, then the entire // arrayOrStringType was a string. - if (arrayType === neverType) { + if (arrayType.flags & TypeFlags.Never) { return stringType; } } @@ -16329,7 +16359,7 @@ namespace ts { if (func) { const signature = getSignatureFromDeclaration(func); const returnType = getReturnTypeOfSignature(signature); - if (strictNullChecks || node.expression || returnType === neverType) { + if (strictNullChecks || node.expression || returnType.flags & TypeFlags.Never) { const exprType = node.expression ? checkExpressionCached(node.expression) : undefinedType; if (func.asteriskToken) { diff --git a/tests/baselines/reference/controlFlowWithIncompleteTypes.js b/tests/baselines/reference/controlFlowWithIncompleteTypes.js new file mode 100644 index 00000000000..83940df6753 --- /dev/null +++ b/tests/baselines/reference/controlFlowWithIncompleteTypes.js @@ -0,0 +1,53 @@ +//// [controlFlowWithIncompleteTypes.ts] +// Repro from #11000 + +declare var cond: boolean; + +function foo1() { + let x: string | number | boolean = 0; + while (cond) { + if (typeof x === "string") { + x = x.slice(); + } + else { + x = "abc"; + } + } +} + +function foo2() { + let x: string | number | boolean = 0; + while (cond) { + if (typeof x === "number") { + x = "abc"; + } + else { + x = x.slice(); + } + } +} + +//// [controlFlowWithIncompleteTypes.js] +// Repro from #11000 +function foo1() { + var x = 0; + while (cond) { + if (typeof x === "string") { + x = x.slice(); + } + else { + x = "abc"; + } + } +} +function foo2() { + var x = 0; + while (cond) { + if (typeof x === "number") { + x = "abc"; + } + else { + x = x.slice(); + } + } +} diff --git a/tests/baselines/reference/controlFlowWithIncompleteTypes.symbols b/tests/baselines/reference/controlFlowWithIncompleteTypes.symbols new file mode 100644 index 00000000000..2ad709140cb --- /dev/null +++ b/tests/baselines/reference/controlFlowWithIncompleteTypes.symbols @@ -0,0 +1,55 @@ +=== tests/cases/compiler/controlFlowWithIncompleteTypes.ts === +// Repro from #11000 + +declare var cond: boolean; +>cond : Symbol(cond, Decl(controlFlowWithIncompleteTypes.ts, 2, 11)) + +function foo1() { +>foo1 : Symbol(foo1, Decl(controlFlowWithIncompleteTypes.ts, 2, 26)) + + let x: string | number | boolean = 0; +>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 5, 7)) + + while (cond) { +>cond : Symbol(cond, Decl(controlFlowWithIncompleteTypes.ts, 2, 11)) + + if (typeof x === "string") { +>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 5, 7)) + + x = x.slice(); +>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 5, 7)) +>x.slice : Symbol(String.slice, Decl(lib.d.ts, --, --)) +>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 5, 7)) +>slice : Symbol(String.slice, Decl(lib.d.ts, --, --)) + } + else { + x = "abc"; +>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 5, 7)) + } + } +} + +function foo2() { +>foo2 : Symbol(foo2, Decl(controlFlowWithIncompleteTypes.ts, 14, 1)) + + let x: string | number | boolean = 0; +>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 17, 7)) + + while (cond) { +>cond : Symbol(cond, Decl(controlFlowWithIncompleteTypes.ts, 2, 11)) + + if (typeof x === "number") { +>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 17, 7)) + + x = "abc"; +>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 17, 7)) + } + else { + x = x.slice(); +>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 17, 7)) +>x.slice : Symbol(String.slice, Decl(lib.d.ts, --, --)) +>x : Symbol(x, Decl(controlFlowWithIncompleteTypes.ts, 17, 7)) +>slice : Symbol(String.slice, Decl(lib.d.ts, --, --)) + } + } +} diff --git a/tests/baselines/reference/controlFlowWithIncompleteTypes.types b/tests/baselines/reference/controlFlowWithIncompleteTypes.types new file mode 100644 index 00000000000..784f22a3966 --- /dev/null +++ b/tests/baselines/reference/controlFlowWithIncompleteTypes.types @@ -0,0 +1,71 @@ +=== tests/cases/compiler/controlFlowWithIncompleteTypes.ts === +// Repro from #11000 + +declare var cond: boolean; +>cond : boolean + +function foo1() { +>foo1 : () => void + + let x: string | number | boolean = 0; +>x : string | number | boolean +>0 : 0 + + while (cond) { +>cond : boolean + + if (typeof x === "string") { +>typeof x === "string" : boolean +>typeof x : string +>x : string | number +>"string" : "string" + + x = x.slice(); +>x = x.slice() : string +>x : string | number | boolean +>x.slice() : string +>x.slice : (start?: number, end?: number) => string +>x : string +>slice : (start?: number, end?: number) => string + } + else { + x = "abc"; +>x = "abc" : "abc" +>x : string | number | boolean +>"abc" : "abc" + } + } +} + +function foo2() { +>foo2 : () => void + + let x: string | number | boolean = 0; +>x : string | number | boolean +>0 : 0 + + while (cond) { +>cond : boolean + + if (typeof x === "number") { +>typeof x === "number" : boolean +>typeof x : string +>x : string | number +>"number" : "number" + + x = "abc"; +>x = "abc" : "abc" +>x : string | number | boolean +>"abc" : "abc" + } + else { + x = x.slice(); +>x = x.slice() : string +>x : string | number | boolean +>x.slice() : string +>x.slice : (start?: number, end?: number) => string +>x : string +>slice : (start?: number, end?: number) => string + } + } +} diff --git a/tests/cases/compiler/controlFlowWithIncompleteTypes.ts b/tests/cases/compiler/controlFlowWithIncompleteTypes.ts new file mode 100644 index 00000000000..a851e6652c8 --- /dev/null +++ b/tests/cases/compiler/controlFlowWithIncompleteTypes.ts @@ -0,0 +1,27 @@ +// Repro from #11000 + +declare var cond: boolean; + +function foo1() { + let x: string | number | boolean = 0; + while (cond) { + if (typeof x === "string") { + x = x.slice(); + } + else { + x = "abc"; + } + } +} + +function foo2() { + let x: string | number | boolean = 0; + while (cond) { + if (typeof x === "number") { + x = "abc"; + } + else { + x = x.slice(); + } + } +} \ No newline at end of file