Skip to content

Commit ce15646

Browse files
committed
Narrow type in case/default sections in switch on discriminant property
1 parent 4a8f94a commit ce15646

3 files changed

Lines changed: 102 additions & 23 deletions

File tree

src/compiler/binder.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -693,8 +693,23 @@ namespace ts {
693693
setFlowNodeReferenced(antecedent);
694694
return <FlowCondition>{
695695
flags,
696-
antecedent,
697696
expression,
697+
antecedent
698+
};
699+
}
700+
701+
function createFlowSwitchClause(antecedent: FlowNode, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): FlowNode {
702+
const expr = switchStatement.expression;
703+
if (expr.kind !== SyntaxKind.PropertyAccessExpression || !isNarrowableReference((<PropertyAccessExpression>expr).expression)) {
704+
return antecedent;
705+
}
706+
setFlowNodeReferenced(antecedent);
707+
return <FlowSwitchClause>{
708+
flags: FlowFlags.SwitchClause,
709+
switchStatement,
710+
clauseStart,
711+
clauseEnd,
712+
antecedent
698713
};
699714
}
700715

@@ -923,9 +938,9 @@ namespace ts {
923938
preSwitchCaseFlow = currentFlow;
924939
bind(node.caseBlock);
925940
addAntecedent(postSwitchLabel, currentFlow);
926-
const hasNonEmptyDefault = forEach(node.caseBlock.clauses, c => c.kind === SyntaxKind.DefaultClause && c.statements.length);
927-
if (!hasNonEmptyDefault) {
928-
addAntecedent(postSwitchLabel, preSwitchCaseFlow);
941+
const hasDefault = forEach(node.caseBlock.clauses, c => c.kind === SyntaxKind.DefaultClause);
942+
if (!hasDefault) {
943+
addAntecedent(postSwitchLabel, createFlowSwitchClause(preSwitchCaseFlow, node, 0, 0));
929944
}
930945
currentBreakTarget = saveBreakTarget;
931946
preSwitchCaseFlow = savePreSwitchCaseFlow;
@@ -934,25 +949,22 @@ namespace ts {
934949

935950
function bindCaseBlock(node: CaseBlock): void {
936951
const clauses = node.clauses;
952+
let fallthroughFlow = unreachableFlow;
937953
for (let i = 0; i < clauses.length; i++) {
938-
const clause = clauses[i];
939-
if (clause.statements.length) {
940-
if (currentFlow.flags & FlowFlags.Unreachable) {
941-
currentFlow = preSwitchCaseFlow;
942-
}
943-
else {
944-
const preCaseLabel = createBranchLabel();
945-
addAntecedent(preCaseLabel, preSwitchCaseFlow);
946-
addAntecedent(preCaseLabel, currentFlow);
947-
currentFlow = finishFlowLabel(preCaseLabel);
948-
}
949-
bind(clause);
950-
if (!(currentFlow.flags & FlowFlags.Unreachable) && i !== clauses.length - 1 && options.noFallthroughCasesInSwitch) {
951-
errorOnFirstToken(clause, Diagnostics.Fallthrough_case_in_switch);
952-
}
954+
const clauseStart = i;
955+
while (!clauses[i].statements.length && i + 1 < clauses.length) {
956+
bind(clauses[i]);
957+
i++;
953958
}
954-
else {
955-
bind(clause);
959+
const preCaseLabel = createBranchLabel();
960+
addAntecedent(preCaseLabel, createFlowSwitchClause(preSwitchCaseFlow, <SwitchStatement>node.parent, clauseStart, i + 1));
961+
addAntecedent(preCaseLabel, fallthroughFlow);
962+
currentFlow = finishFlowLabel(preCaseLabel);
963+
const clause = clauses[i];
964+
bind(clause);
965+
fallthroughFlow = currentFlow;
966+
if (!(currentFlow.flags & FlowFlags.Unreachable) && i !== clauses.length - 1 && options.noFallthroughCasesInSwitch) {
967+
errorOnFirstToken(clause, Diagnostics.Fallthrough_case_in_switch);
956968
}
957969
}
958970
}

src/compiler/checker.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7676,6 +7676,29 @@ namespace ts {
76767676
return node;
76777677
}
76787678

7679+
function getTypeOfSwitchClause(clause: CaseClause | DefaultClause) {
7680+
if (clause.kind === SyntaxKind.CaseClause) {
7681+
const expr = (<CaseClause>clause).expression;
7682+
return expr.kind === SyntaxKind.StringLiteral ? getStringLiteralTypeForText((<StringLiteral>expr).text) : checkExpression(expr);
7683+
}
7684+
return undefined;
7685+
}
7686+
7687+
function getSwitchClauseTypes(switchStatement: SwitchStatement): Type[] {
7688+
const links = getNodeLinks(switchStatement);
7689+
if (!links.switchTypes) {
7690+
// If all case clauses specify expressions that have unit types, we return an array
7691+
// of those unit types. Otherwise we return an empty array.
7692+
const types = map(switchStatement.caseBlock.clauses, getTypeOfSwitchClause);
7693+
links.switchTypes = forEach(types, t => !t || t.flags & TypeFlags.StringLiteral) ? types : emptyArray;
7694+
}
7695+
return links.switchTypes;
7696+
}
7697+
7698+
function eachTypeContainedIn(source: Type, types: Type[]) {
7699+
return source.flags & TypeFlags.Union ? !forEach((<UnionType>source).types, t => !contains(types, t)) : contains(types, source);
7700+
}
7701+
76797702
function getFlowTypeOfReference(reference: Node, declaredType: Type, assumeInitialized: boolean, includeOuterFunctions: boolean) {
76807703
let key: string;
76817704
if (!reference.flowNode || assumeInitialized && !(declaredType.flags & TypeFlags.Narrowable)) {
@@ -7713,6 +7736,9 @@ namespace ts {
77137736
else if (flow.flags & FlowFlags.Condition) {
77147737
type = getTypeAtFlowCondition(<FlowCondition>flow);
77157738
}
7739+
else if (flow.flags & FlowFlags.SwitchClause) {
7740+
type = getTypeAtSwitchClause(<FlowSwitchClause>flow);
7741+
}
77167742
else if (flow.flags & FlowFlags.Label) {
77177743
if ((<FlowLabel>flow).antecedents.length === 1) {
77187744
flow = (<FlowLabel>flow).antecedents[0];
@@ -7796,6 +7822,11 @@ namespace ts {
77967822
return type;
77977823
}
77987824

7825+
function getTypeAtSwitchClause(flow: FlowSwitchClause) {
7826+
const type = getTypeAtFlowNode(flow.antecedent);
7827+
return narrowTypeBySwitchOnDiscriminant(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
7828+
}
7829+
77997830
function getTypeAtFlowBranchLabel(flow: FlowLabel) {
78007831
const antecedentTypes: Type[] = [];
78017832
for (const antecedent of flow.antecedents) {
@@ -7938,6 +7969,33 @@ namespace ts {
79387969
return type;
79397970
}
79407971

7972+
function narrowTypeBySwitchOnDiscriminant(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {
7973+
// We have switch statement with property access expression
7974+
if (!(type.flags & TypeFlags.Union) || !isMatchingReference(reference, (<PropertyAccessExpression>switchStatement.expression).expression)) {
7975+
return type;
7976+
}
7977+
const propName = (<PropertyAccessExpression>switchStatement.expression).name.text;
7978+
const propType = getTypeOfPropertyOfType(type, propName);
7979+
if (!propType || !isStringLiteralUnionType(propType)) {
7980+
return type;
7981+
}
7982+
const switchTypes = getSwitchClauseTypes(switchStatement);
7983+
if (!switchTypes.length) {
7984+
return type;
7985+
}
7986+
const types = (<UnionType>type).types;
7987+
const clauseTypes = switchTypes.slice(clauseStart, clauseEnd);
7988+
const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, undefined);
7989+
const caseTypes = hasDefaultClause ? filter(clauseTypes, t => !!t) : clauseTypes;
7990+
const discriminantType = caseTypes.length ? getUnionType(caseTypes) : undefined;
7991+
const caseType = discriminantType && getUnionType(filter(types, t => isTypeComparableTo(discriminantType, getTypeOfPropertyOfType(t, propName))));
7992+
if (!hasDefaultClause) {
7993+
return caseType;
7994+
}
7995+
const defaultType = getUnionType(filter(types, t => !eachTypeContainedIn(getTypeOfPropertyOfType(t, propName), switchTypes)));
7996+
return caseType ? getUnionType([caseType, defaultType]) : defaultType;
7997+
}
7998+
79417999
function narrowTypeByTypeof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
79428000
// We have '==', '!=', '====', or !==' operator with 'typeof xxx' on the left
79438001
// and string literal on the right

src/compiler/types.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,8 +1554,9 @@ namespace ts {
15541554
Assignment = 1 << 4, // Assignment
15551555
TrueCondition = 1 << 5, // Condition known to be true
15561556
FalseCondition = 1 << 6, // Condition known to be false
1557-
Referenced = 1 << 7, // Referenced as antecedent once
1558-
Shared = 1 << 8, // Referenced as antecedent more than once
1557+
SwitchClause = 1 << 7, // Switch statement clause
1558+
Referenced = 1 << 8, // Referenced as antecedent once
1559+
Shared = 1 << 9, // Referenced as antecedent more than once
15591560
Label = BranchLabel | LoopLabel,
15601561
Condition = TrueCondition | FalseCondition
15611562
}
@@ -1591,6 +1592,13 @@ namespace ts {
15911592
antecedent: FlowNode;
15921593
}
15931594

1595+
export interface FlowSwitchClause extends FlowNode {
1596+
switchStatement: SwitchStatement;
1597+
clauseStart: number; // Start index of case/default clause range
1598+
clauseEnd: number; // End index of case/default clause range
1599+
antecedent: FlowNode;
1600+
}
1601+
15941602
export interface AmdDependency {
15951603
path: string;
15961604
name: string;
@@ -2170,6 +2178,7 @@ namespace ts {
21702178
resolvedJsxType?: Type; // resolved element attributes type of a JSX openinglike element
21712179
hasSuperCall?: boolean; // recorded result when we try to find super-call. We only try to find one if this flag is undefined, indicating that we haven't made an attempt.
21722180
superCall?: ExpressionStatement; // Cached first super-call found in the constructor. Used in checking whether super is called before this-accessing
2181+
switchTypes?: Type[]; // Cached array of switch case expression types
21732182
}
21742183

21752184
export const enum TypeFlags {

0 commit comments

Comments
 (0)