Skip to content

Commit 49acbe8

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 0f47c8a commit 49acbe8

File tree

6 files changed

+1975
-0
lines changed

6 files changed

+1975
-0
lines changed

src/compiler/binder.ts

+2
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,8 @@ namespace ts {
735735
return isNarrowingBinaryExpression(<BinaryExpression>expr);
736736
case SyntaxKind.PrefixUnaryExpression:
737737
return (<PrefixUnaryExpression>expr).operator === SyntaxKind.ExclamationToken && isNarrowingExpression((<PrefixUnaryExpression>expr).operand);
738+
case SyntaxKind.TypeOfExpression:
739+
return isNarrowingExpression((<TypeOfExpression>expr).expression);
738740
}
739741
return false;
740742
}

src/compiler/checker.ts

+119
Original file line numberDiff line numberDiff line change
@@ -12526,6 +12526,21 @@ namespace ts {
1252612526
return links.switchTypes;
1252712527
}
1252812528

12529+
function getSwitchClauseTypeOfWitnesses(switchStatement: SwitchStatement): (string | undefined)[] {
12530+
const witnesses: (string | undefined)[] = [];
12531+
for (const clause of switchStatement.caseBlock.clauses) {
12532+
if (clause.kind === SyntaxKind.CaseClause) {
12533+
if (clause.expression.kind === SyntaxKind.StringLiteral) {
12534+
witnesses.push((clause.expression as StringLiteral).text);
12535+
continue;
12536+
}
12537+
return emptyArray;
12538+
}
12539+
witnesses.push(/*explicitDefaultStatement*/ undefined);
12540+
}
12541+
return witnesses;
12542+
}
12543+
1252912544
function eachTypeContainedIn(source: Type, types: Type[]) {
1253012545
return source.flags & TypeFlags.Union ? !forEach((<UnionType>source).types, t => !contains(types, t)) : contains(types, source);
1253112546
}
@@ -12939,6 +12954,9 @@ namespace ts {
1293912954
else if (isMatchingReferenceDiscriminant(expr, type)) {
1294012955
type = narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd));
1294112956
}
12957+
else if (expr.kind === SyntaxKind.TypeOfExpression && isMatchingReference(reference, (expr as TypeOfExpression).expression)) {
12958+
type = narrowBySwitchOnTypeOf(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
12959+
}
1294212960
return createFlowType(type, isIncomplete(flowType));
1294312961
}
1294412962

@@ -13235,6 +13253,57 @@ namespace ts {
1323513253
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
1323613254
}
1323713255

13256+
function narrowBySwitchOnTypeOf(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): Type {
13257+
const switchWitnesses = getSwitchClauseTypeOfWitnesses(switchStatement);
13258+
if (!switchWitnesses.length) {
13259+
return type;
13260+
}
13261+
const clauseWitnesses = switchWitnesses.slice(clauseStart, clauseEnd);
13262+
// Equal start and end denotes implicit fallthrough; undefined marks explicit default clause
13263+
const hasDefaultClause = clauseStart === clauseEnd || contains(clauseWitnesses, /*explicitDefaultStatement*/ undefined);
13264+
const switchFacts = getFactsFromTypeofSwitch(clauseStart, clauseEnd, switchWitnesses, hasDefaultClause);
13265+
// The implied type is the raw type suggested by a
13266+
// value being caught in this clause.
13267+
// - If there is a default the implied type is not used.
13268+
// - Otherwise, take the union of the types in the
13269+
// clause. We narrow the union using facts to remove
13270+
// types that appear multiple types and are
13271+
// unreachable.
13272+
// Example:
13273+
//
13274+
// switch (typeof x) {
13275+
// case 'number':
13276+
// case 'string': break;
13277+
// default: break;
13278+
// case 'number':
13279+
// case 'boolean': break
13280+
// }
13281+
//
13282+
// The implied type of the first clause number | string.
13283+
// The implied type of the second clause is string (but this doesn't get used).
13284+
// The implied type of the third clause is boolean (number has already be caught).
13285+
if (!(hasDefaultClause || (type.flags & TypeFlags.Union))) {
13286+
let impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => typeofTypesByName.get(text) || neverType)), switchFacts);
13287+
if (impliedType.flags & TypeFlags.Union) {
13288+
impliedType = getAssignmentReducedType(impliedType as UnionType, getBaseConstraintOfType(type) || type);
13289+
}
13290+
if (!(impliedType.flags & TypeFlags.Never)) {
13291+
if (isTypeSubtypeOf(impliedType, type)) {
13292+
return impliedType;
13293+
}
13294+
if (type.flags & TypeFlags.Instantiable) {
13295+
const constraint = getBaseConstraintOfType(type) || anyType;
13296+
if (isTypeSubtypeOf(impliedType, constraint)) {
13297+
return getIntersectionType([type, impliedType]);
13298+
}
13299+
}
13300+
}
13301+
}
13302+
return hasDefaultClause ?
13303+
filterType(type, t => (getTypeFacts(t) & switchFacts) === switchFacts) :
13304+
getTypeWithFacts(type, switchFacts);
13305+
}
13306+
1323813307
function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
1323913308
const left = getReferenceCandidate(expr.left);
1324013309
if (!isMatchingReference(reference, left)) {
@@ -18540,10 +18609,60 @@ namespace ts {
1854018609
: Diagnostics.Type_of_yield_operand_in_an_async_generator_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member);
1854118610
}
1854218611

18612+
/**
18613+
* Collect the TypeFacts learned from a typeof switch with
18614+
* total clauses `witnesses`, and the active clause ranging
18615+
* from `start` to `end`. Parameter `hasDefault` denotes
18616+
* whether the active clause contains a default clause.
18617+
*/
18618+
function getFactsFromTypeofSwitch(start: number, end: number, witnesses: (string | undefined)[], hasDefault: boolean): TypeFacts {
18619+
let facts: TypeFacts = TypeFacts.None;
18620+
// When in the default we only collect inequality facts
18621+
// because default is 'in theory' a set of infinite
18622+
// equalities.
18623+
if (hasDefault) {
18624+
// Value is not equal to any types after the active clause.
18625+
for (let i = end; i < witnesses.length; i++) {
18626+
facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject;
18627+
}
18628+
// Remove inequalities for types that appear in the
18629+
// active clause because they appear before other
18630+
// types collected so far.
18631+
for (let i = start; i < end; i++) {
18632+
facts &= ~(typeofNEFacts.get(witnesses[i]) || 0);
18633+
}
18634+
// Add inequalities for types before the active clause unconditionally.
18635+
for (let i = 0; i < start; i++) {
18636+
facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject;
18637+
}
18638+
}
18639+
// When in an active clause without default the set of
18640+
// equalities is finite.
18641+
else {
18642+
// Add equalities for all types in the active clause.
18643+
for (let i = start; i < end; i++) {
18644+
facts |= typeofEQFacts.get(witnesses[i]) || TypeFacts.TypeofEQHostObject;
18645+
}
18646+
// Remove equalities for types that appear before the
18647+
// active clause.
18648+
for (let i = 0; i < start; i++) {
18649+
facts &= ~(typeofEQFacts.get(witnesses[i]) || 0);
18650+
}
18651+
}
18652+
return facts;
18653+
}
18654+
1854318655
function isExhaustiveSwitchStatement(node: SwitchStatement): boolean {
1854418656
if (!node.possiblyExhaustive) {
1854518657
return false;
1854618658
}
18659+
if (node.expression.kind === SyntaxKind.TypeOfExpression) {
18660+
const operandType = getTypeOfExpression((node.expression as TypeOfExpression).expression);
18661+
// Type is not equal to every type in the switch.
18662+
const notEqualFacts = getFactsFromTypeofSwitch(0, 0, getSwitchClauseTypeOfWitnesses(node), /*hasDefault*/ true);
18663+
const type = getBaseConstraintOfType(operandType) || operandType;
18664+
return !!(filterType(type, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts).flags & TypeFlags.Never);
18665+
}
1854718666
const type = getTypeOfExpression(node.expression);
1854818667
if (!isLiteralType(type)) {
1854918668
return false;

0 commit comments

Comments
 (0)