Skip to content

Commit 8f654f0

Browse files
Merge pull request #21957 from jack-williams/typeof-in-switch
Fix #2214. Support narrowing with typeof in switch condition.
2 parents 8c22770 + 3173cfe commit 8f654f0

File tree

6 files changed

+2136
-0
lines changed

6 files changed

+2136
-0
lines changed

src/compiler/binder.ts

Lines changed: 2 additions & 0 deletions
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

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14282,6 +14282,23 @@ namespace ts {
1428214282
return links.switchTypes;
1428314283
}
1428414284

14285+
// Get the types from all cases in a switch on `typeof`. An
14286+
// `undefined` element denotes an explicit `default` clause.
14287+
function getSwitchClauseTypeOfWitnesses(switchStatement: SwitchStatement): (string | undefined)[] {
14288+
const witnesses: (string | undefined)[] = [];
14289+
for (const clause of switchStatement.caseBlock.clauses) {
14290+
if (clause.kind === SyntaxKind.CaseClause) {
14291+
if (clause.expression.kind === SyntaxKind.StringLiteral) {
14292+
witnesses.push((clause.expression as StringLiteral).text);
14293+
continue;
14294+
}
14295+
return emptyArray;
14296+
}
14297+
witnesses.push(/*explicitDefaultStatement*/ undefined);
14298+
}
14299+
return witnesses;
14300+
}
14301+
1428514302
function eachTypeContainedIn(source: Type, types: Type[]) {
1428614303
return source.flags & TypeFlags.Union ? !forEach((<UnionType>source).types, t => !contains(types, t)) : contains(types, source);
1428714304
}
@@ -14704,6 +14721,9 @@ namespace ts {
1470414721
expr as PropertyAccessExpression | ElementAccessExpression,
1470514722
t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd));
1470614723
}
14724+
else if (expr.kind === SyntaxKind.TypeOfExpression && isMatchingReference(reference, (expr as TypeOfExpression).expression)) {
14725+
type = narrowBySwitchOnTypeOf(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
14726+
}
1470714727
return createFlowType(type, isIncomplete(flowType));
1470814728
}
1470914729

@@ -15019,6 +15039,83 @@ namespace ts {
1501915039
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
1502015040
}
1502115041

15042+
function narrowBySwitchOnTypeOf(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): Type {
15043+
const switchWitnesses = getSwitchClauseTypeOfWitnesses(switchStatement);
15044+
if (!switchWitnesses.length) {
15045+
return type;
15046+
}
15047+
// Equal start and end denotes implicit fallthrough; undefined marks explicit default clause
15048+
const defaultCaseLocation = findIndex(switchWitnesses, elem => elem === undefined);
15049+
const hasDefaultClause = clauseStart === clauseEnd || (defaultCaseLocation >= clauseStart && defaultCaseLocation < clauseEnd);
15050+
let clauseWitnesses: string[];
15051+
let switchFacts: TypeFacts;
15052+
if (defaultCaseLocation > -1) {
15053+
// We no longer need the undefined denoting an
15054+
// explicit default case. Remove the undefined and
15055+
// fix-up clauseStart and clauseEnd. This means
15056+
// that we don't have to worry about undefined
15057+
// in the witness array.
15058+
const witnesses = <string[]>switchWitnesses.filter(witness => witness !== undefined);
15059+
// The adjust clause start and end after removing the `default` statement.
15060+
const fixedClauseStart = defaultCaseLocation < clauseStart ? clauseStart - 1 : clauseStart;
15061+
const fixedClauseEnd = defaultCaseLocation < clauseEnd ? clauseEnd - 1 : clauseEnd;
15062+
clauseWitnesses = witnesses.slice(fixedClauseStart, fixedClauseEnd);
15063+
switchFacts = getFactsFromTypeofSwitch(fixedClauseStart, fixedClauseEnd, witnesses, hasDefaultClause);
15064+
}
15065+
else {
15066+
clauseWitnesses = <string[]>switchWitnesses.slice(clauseStart, clauseEnd);
15067+
switchFacts = getFactsFromTypeofSwitch(clauseStart, clauseEnd, <string[]>switchWitnesses, hasDefaultClause);
15068+
}
15069+
/*
15070+
The implied type is the raw type suggested by a
15071+
value being caught in this clause.
15072+
15073+
When the clause contains a default case we ignore
15074+
the implied type and try to narrow using any facts
15075+
we can learn: see `switchFacts`.
15076+
15077+
Example:
15078+
switch (typeof x) {
15079+
case 'number':
15080+
case 'string': break;
15081+
default: break;
15082+
case 'number':
15083+
case 'boolean': break
15084+
}
15085+
15086+
In the first clause (case `number` and `string`) the
15087+
implied type is number | string.
15088+
15089+
In the default clause we de not compute an implied type.
15090+
15091+
In the third clause (case `number` and `boolean`)
15092+
the naive implied type is number | boolean, however
15093+
we use the type facts to narrow the implied type to
15094+
boolean. We know that number cannot be selected
15095+
because it is caught in the first clause.
15096+
*/
15097+
if (!(hasDefaultClause || (type.flags & TypeFlags.Union))) {
15098+
let impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => typeofTypesByName.get(text) || neverType)), switchFacts);
15099+
if (impliedType.flags & TypeFlags.Union) {
15100+
impliedType = getAssignmentReducedType(impliedType as UnionType, getBaseConstraintOfType(type) || type);
15101+
}
15102+
if (!(impliedType.flags & TypeFlags.Never)) {
15103+
if (isTypeSubtypeOf(impliedType, type)) {
15104+
return impliedType;
15105+
}
15106+
if (type.flags & TypeFlags.Instantiable) {
15107+
const constraint = getBaseConstraintOfType(type) || anyType;
15108+
if (isTypeSubtypeOf(impliedType, constraint)) {
15109+
return getIntersectionType([type, impliedType]);
15110+
}
15111+
}
15112+
}
15113+
}
15114+
return hasDefaultClause ?
15115+
filterType(type, t => (getTypeFacts(t) & switchFacts) === switchFacts) :
15116+
getTypeWithFacts(type, switchFacts);
15117+
}
15118+
1502215119
function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
1502315120
const left = getReferenceCandidate(expr.left);
1502415121
if (!isMatchingReference(reference, left)) {
@@ -20572,10 +20669,62 @@ namespace ts {
2057220669
: Diagnostics.Type_of_yield_operand_in_an_async_generator_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member);
2057320670
}
2057420671

20672+
/**
20673+
* Collect the TypeFacts learned from a typeof switch with
20674+
* total clauses `witnesses`, and the active clause ranging
20675+
* from `start` to `end`. Parameter `hasDefault` denotes
20676+
* whether the active clause contains a default clause.
20677+
*/
20678+
function getFactsFromTypeofSwitch(start: number, end: number, witnesses: string[], hasDefault: boolean): TypeFacts {
20679+
let facts: TypeFacts = TypeFacts.None;
20680+
// When in the default we only collect inequality facts
20681+
// because default is 'in theory' a set of infinite
20682+
// equalities.
20683+
if (hasDefault) {
20684+
// Value is not equal to any types after the active clause.
20685+
for (let i = end; i < witnesses.length; i++) {
20686+
facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject;
20687+
}
20688+
// Remove inequalities for types that appear in the
20689+
// active clause because they appear before other
20690+
// types collected so far.
20691+
for (let i = start; i < end; i++) {
20692+
facts &= ~(typeofNEFacts.get(witnesses[i]) || 0);
20693+
}
20694+
// Add inequalities for types before the active clause unconditionally.
20695+
for (let i = 0; i < start; i++) {
20696+
facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject;
20697+
}
20698+
}
20699+
// When in an active clause without default the set of
20700+
// equalities is finite.
20701+
else {
20702+
// Add equalities for all types in the active clause.
20703+
for (let i = start; i < end; i++) {
20704+
facts |= typeofEQFacts.get(witnesses[i]) || TypeFacts.TypeofEQHostObject;
20705+
}
20706+
// Remove equalities for types that appear before the
20707+
// active clause.
20708+
for (let i = 0; i < start; i++) {
20709+
facts &= ~(typeofEQFacts.get(witnesses[i]) || 0);
20710+
}
20711+
}
20712+
return facts;
20713+
}
20714+
2057520715
function isExhaustiveSwitchStatement(node: SwitchStatement): boolean {
2057620716
if (!node.possiblyExhaustive) {
2057720717
return false;
2057820718
}
20719+
if (node.expression.kind === SyntaxKind.TypeOfExpression) {
20720+
const operandType = getTypeOfExpression((node.expression as TypeOfExpression).expression);
20721+
// This cast is safe because the switch is possibly exhaustive and does not contain a default case, so there can be no undefined.
20722+
const witnesses = <string[]>getSwitchClauseTypeOfWitnesses(node);
20723+
// notEqualFacts states that the type of the switched value is not equal to every type in the switch.
20724+
const notEqualFacts = getFactsFromTypeofSwitch(0, 0, witnesses, /*hasDefault*/ true);
20725+
const type = getBaseConstraintOfType(operandType) || operandType;
20726+
return !!(filterType(type, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts).flags & TypeFlags.Never);
20727+
}
2057920728
const type = getTypeOfExpression(node.expression);
2058020729
if (!isLiteralType(type)) {
2058120730
return false;

0 commit comments

Comments
 (0)