Skip to content

Commit 0d79831

Browse files
committed
Add typeof-for-switch
Initial draft that works for union types First draft of PR ready code with tests Revert changed line for testing Add exhaustiveness checking and move narrowByTypeOfWitnesses Try caching mechanism Comment out exhaustiveness checking to find perf regression Re-enable exhaustiveness checking for typeof switches Check if changes to narrowByTypeOfWitnesses fix perf alone. Improve switch narrowing: + Take into account repeated clauses in the switch. + Handle unions of constrained type parameters. Add more tests Comments Revert back to if-like behaviour Remove redundant checks and simplify exhaustiveness checks Change comment for narrowBySwitchOnTypeOf Reduce implied type with getAssignmentReducedType Remove any annotations
1 parent b271df1 commit 0d79831

File tree

6 files changed

+1975
-0
lines changed

6 files changed

+1975
-0
lines changed

src/compiler/binder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,8 @@ namespace ts {
737737
return isNarrowingBinaryExpression(<BinaryExpression>expr);
738738
case SyntaxKind.PrefixUnaryExpression:
739739
return (<PrefixUnaryExpression>expr).operator === SyntaxKind.ExclamationToken && isNarrowingExpression((<PrefixUnaryExpression>expr).operand);
740+
case SyntaxKind.TypeOfExpression:
741+
return isNarrowingExpression((<TypeOfExpression>expr).expression);
740742
}
741743
return false;
742744
}

src/compiler/checker.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12840,6 +12840,21 @@ namespace ts {
1284012840
return links.switchTypes;
1284112841
}
1284212842

12843+
function getSwitchClauseTypeOfWitnesses(switchStatement: SwitchStatement): (string | undefined)[] {
12844+
const witnesses: (string | undefined)[] = [];
12845+
for (const clause of switchStatement.caseBlock.clauses) {
12846+
if (clause.kind === SyntaxKind.CaseClause) {
12847+
if (clause.expression.kind === SyntaxKind.StringLiteral) {
12848+
witnesses.push((clause.expression as StringLiteral).text);
12849+
continue;
12850+
}
12851+
return emptyArray;
12852+
}
12853+
witnesses.push(/*explicitDefaultStatement*/ undefined);
12854+
}
12855+
return witnesses;
12856+
}
12857+
1284312858
function eachTypeContainedIn(source: Type, types: Type[]) {
1284412859
return source.flags & TypeFlags.Union ? !forEach((<UnionType>source).types, t => !contains(types, t)) : contains(types, source);
1284512860
}
@@ -13253,6 +13268,9 @@ namespace ts {
1325313268
else if (isMatchingReferenceDiscriminant(expr, type)) {
1325413269
type = narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd));
1325513270
}
13271+
else if (expr.kind === SyntaxKind.TypeOfExpression && isMatchingReference(reference, (expr as TypeOfExpression).expression)) {
13272+
type = narrowBySwitchOnTypeOf(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
13273+
}
1325613274
return createFlowType(type, isIncomplete(flowType));
1325713275
}
1325813276

@@ -13549,6 +13567,57 @@ namespace ts {
1354913567
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
1355013568
}
1355113569

13570+
function narrowBySwitchOnTypeOf(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): Type {
13571+
const switchWitnesses = getSwitchClauseTypeOfWitnesses(switchStatement);
13572+
if (!switchWitnesses.length) {
13573+
return type;
13574+
}
13575+
const clauseWitnesses = switchWitnesses.slice(clauseStart, clauseEnd);
13576+
// Equal start and end denotes implicit fallthrough; undefined marks explicit default clause
13577+
const hasDefaultClause = clauseStart === clauseEnd || contains(clauseWitnesses, /*explicitDefaultStatement*/ undefined);
13578+
const switchFacts = getFactsFromTypeofSwitch(clauseStart, clauseEnd, switchWitnesses, hasDefaultClause);
13579+
// The implied type is the raw type suggested by a
13580+
// value being caught in this clause.
13581+
// - If there is a default the implied type is not used.
13582+
// - Otherwise, take the union of the types in the
13583+
// clause. We narrow the union using facts to remove
13584+
// types that appear multiple types and are
13585+
// unreachable.
13586+
// Example:
13587+
//
13588+
// switch (typeof x) {
13589+
// case 'number':
13590+
// case 'string': break;
13591+
// default: break;
13592+
// case 'number':
13593+
// case 'boolean': break
13594+
// }
13595+
//
13596+
// The implied type of the first clause number | string.
13597+
// The implied type of the second clause is string (but this doesn't get used).
13598+
// The implied type of the third clause is boolean (number has already be caught).
13599+
if (!(hasDefaultClause || (type.flags & TypeFlags.Union))) {
13600+
let impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => typeofTypesByName.get(text) || neverType)), switchFacts);
13601+
if (impliedType.flags & TypeFlags.Union) {
13602+
impliedType = getAssignmentReducedType(impliedType as UnionType, getBaseConstraintOfType(type) || type);
13603+
}
13604+
if (!(impliedType.flags & TypeFlags.Never)) {
13605+
if (isTypeSubtypeOf(impliedType, type)) {
13606+
return impliedType;
13607+
}
13608+
if (type.flags & TypeFlags.Instantiable) {
13609+
const constraint = getBaseConstraintOfType(type) || anyType;
13610+
if (isTypeSubtypeOf(impliedType, constraint)) {
13611+
return getIntersectionType([type, impliedType]);
13612+
}
13613+
}
13614+
}
13615+
}
13616+
return hasDefaultClause ?
13617+
filterType(type, t => (getTypeFacts(t) & switchFacts) === switchFacts) :
13618+
getTypeWithFacts(type, switchFacts);
13619+
}
13620+
1355213621
function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
1355313622
const left = getReferenceCandidate(expr.left);
1355413623
if (!isMatchingReference(reference, left)) {
@@ -18944,10 +19013,60 @@ namespace ts {
1894419013
: Diagnostics.Type_of_yield_operand_in_an_async_generator_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member);
1894519014
}
1894619015

19016+
/**
19017+
* Collect the TypeFacts learned from a typeof switch with
19018+
* total clauses `witnesses`, and the active clause ranging
19019+
* from `start` to `end`. Parameter `hasDefault` denotes
19020+
* whether the active clause contains a default clause.
19021+
*/
19022+
function getFactsFromTypeofSwitch(start: number, end: number, witnesses: (string | undefined)[], hasDefault: boolean): TypeFacts {
19023+
let facts: TypeFacts = TypeFacts.None;
19024+
// When in the default we only collect inequality facts
19025+
// because default is 'in theory' a set of infinite
19026+
// equalities.
19027+
if (hasDefault) {
19028+
// Value is not equal to any types after the active clause.
19029+
for (let i = end; i < witnesses.length; i++) {
19030+
facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject;
19031+
}
19032+
// Remove inequalities for types that appear in the
19033+
// active clause because they appear before other
19034+
// types collected so far.
19035+
for (let i = start; i < end; i++) {
19036+
facts &= ~(typeofNEFacts.get(witnesses[i]) || 0);
19037+
}
19038+
// Add inequalities for types before the active clause unconditionally.
19039+
for (let i = 0; i < start; i++) {
19040+
facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject;
19041+
}
19042+
}
19043+
// When in an active clause without default the set of
19044+
// equalities is finite.
19045+
else {
19046+
// Add equalities for all types in the active clause.
19047+
for (let i = start; i < end; i++) {
19048+
facts |= typeofEQFacts.get(witnesses[i]) || TypeFacts.TypeofEQHostObject;
19049+
}
19050+
// Remove equalities for types that appear before the
19051+
// active clause.
19052+
for (let i = 0; i < start; i++) {
19053+
facts &= ~(typeofEQFacts.get(witnesses[i]) || 0);
19054+
}
19055+
}
19056+
return facts;
19057+
}
19058+
1894719059
function isExhaustiveSwitchStatement(node: SwitchStatement): boolean {
1894819060
if (!node.possiblyExhaustive) {
1894919061
return false;
1895019062
}
19063+
if (node.expression.kind === SyntaxKind.TypeOfExpression) {
19064+
const operandType = getTypeOfExpression((node.expression as TypeOfExpression).expression);
19065+
// Type is not equal to every type in the switch.
19066+
const notEqualFacts = getFactsFromTypeofSwitch(0, 0, getSwitchClauseTypeOfWitnesses(node), /*hasDefault*/ true);
19067+
const type = getBaseConstraintOfType(operandType) || operandType;
19068+
return !!(filterType(type, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts).flags & TypeFlags.Never);
19069+
}
1895119070
const type = getTypeOfExpression(node.expression);
1895219071
if (!isLiteralType(type)) {
1895319072
return false;

0 commit comments

Comments
 (0)