diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 4155e2e195f..e413c20f79c 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -650,6 +650,11 @@ namespace ts { return isNarrowableReference(expr); } + function isNarrowingSwitchStatement(switchStatement: SwitchStatement) { + const expr = switchStatement.expression; + return expr.kind === SyntaxKind.PropertyAccessExpression && isNarrowableReference((expr).expression); + } + function createBranchLabel(): FlowLabel { return { flags: FlowFlags.BranchLabel, @@ -699,8 +704,7 @@ namespace ts { } function createFlowSwitchClause(antecedent: FlowNode, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): FlowNode { - const expr = switchStatement.expression; - if (expr.kind !== SyntaxKind.PropertyAccessExpression || !isNarrowableReference((expr).expression)) { + if (!isNarrowingSwitchStatement(switchStatement)) { return antecedent; } setFlowNodeReferenced(antecedent); @@ -939,6 +943,9 @@ namespace ts { bind(node.caseBlock); addAntecedent(postSwitchLabel, currentFlow); const hasDefault = forEach(node.caseBlock.clauses, c => c.kind === SyntaxKind.DefaultClause); + // We mark a switch statement as possibly exhaustive if it has no default clause and if all + // case clauses have unreachable end points (e.g. they all return). + node.possiblyExhaustive = !hasDefault && !postSwitchLabel.antecedents; if (!hasDefault) { addAntecedent(postSwitchLabel, createFlowSwitchClause(preSwitchCaseFlow, node, 0, 0)); } diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index bb268b55da8..46e683fc0c8 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11808,10 +11808,42 @@ namespace ts { return aggregatedTypes; } + function isExhaustiveSwitchStatement(node: SwitchStatement): boolean { + const expr = node.expression; + if (!node.possiblyExhaustive || expr.kind !== SyntaxKind.PropertyAccessExpression) { + return false; + } + const type = checkExpression((expr).expression); + if (!(type.flags & TypeFlags.Union)) { + return false; + } + const propName = (expr).name.text; + const propType = getTypeOfPropertyOfType(type, propName); + if (!propType || !isStringLiteralUnionType(propType)) { + return false; + } + const switchTypes = getSwitchClauseTypes(node); + if (!switchTypes.length) { + return false; + } + return eachTypeContainedIn(propType, switchTypes); + } + + function functionHasImplicitReturn(func: FunctionLikeDeclaration) { + if (!(func.flags & NodeFlags.HasImplicitReturn)) { + return false; + } + const lastStatement = lastOrUndefined((func.body).statements); + if (lastStatement && lastStatement.kind === SyntaxKind.SwitchStatement && isExhaustiveSwitchStatement(lastStatement)) { + return false; + } + return true; + } + function checkAndAggregateReturnExpressionTypes(func: FunctionLikeDeclaration, contextualMapper: TypeMapper): Type[] { const isAsync = isAsyncFunctionLike(func); const aggregatedTypes: Type[] = []; - let hasReturnWithNoExpression = !!(func.flags & NodeFlags.HasImplicitReturn); + let hasReturnWithNoExpression = functionHasImplicitReturn(func); let hasReturnOfTypeNever = false; forEachReturnStatement(func.body, returnStatement => { const expr = returnStatement.expression; @@ -11868,7 +11900,7 @@ namespace ts { // If all we have is a function signature, or an arrow function with an expression body, then there is nothing to check. // also if HasImplicitReturn flag is not set this means that all codepaths in function body end with return or throw - if (nodeIsMissing(func.body) || func.body.kind !== SyntaxKind.Block || !(func.flags & NodeFlags.HasImplicitReturn)) { + if (nodeIsMissing(func.body) || func.body.kind !== SyntaxKind.Block || !functionHasImplicitReturn(func)) { return; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index fbcdc594656..d44023ac773 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1188,6 +1188,7 @@ namespace ts { export interface SwitchStatement extends Statement { expression: Expression; caseBlock: CaseBlock; + possiblyExhaustive?: boolean; } // @kind(SyntaxKind.CaseBlock)