Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #2214. Support narrowing with typeof in switch condition. #21957

Merged
merged 8 commits into from
Sep 6, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,8 @@ namespace ts {
return isNarrowingBinaryExpression(<BinaryExpression>expr);
case SyntaxKind.PrefixUnaryExpression:
return (<PrefixUnaryExpression>expr).operator === SyntaxKind.ExclamationToken && isNarrowingExpression((<PrefixUnaryExpression>expr).operand);
case SyntaxKind.TypeOfExpression:
return isNarrowingExpression((<TypeOfExpression>expr).expression);
}
return false;
}
Expand Down
149 changes: 149 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13920,6 +13920,23 @@ namespace ts {
return links.switchTypes;
}

// Get the types from all cases in a switch on `typeof`. An
// `undefined` element denotes an explicit `default` clause.
function getSwitchClauseTypeOfWitnesses(switchStatement: SwitchStatement): (string | undefined)[] {
const witnesses: (string | undefined)[] = [];
for (const clause of switchStatement.caseBlock.clauses) {
if (clause.kind === SyntaxKind.CaseClause) {
if (clause.expression.kind === SyntaxKind.StringLiteral) {
witnesses.push((clause.expression as StringLiteral).text);
continue;
}
return emptyArray;
}
witnesses.push(/*explicitDefaultStatement*/ undefined);
}
return witnesses;
}

function eachTypeContainedIn(source: Type, types: Type[]) {
return source.flags & TypeFlags.Union ? !forEach((<UnionType>source).types, t => !contains(types, t)) : contains(types, source);
}
Expand Down Expand Up @@ -14335,6 +14352,9 @@ namespace ts {
else if (isMatchingReferenceDiscriminant(expr, type)) {
type = narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd));
}
else if (expr.kind === SyntaxKind.TypeOfExpression && isMatchingReference(reference, (expr as TypeOfExpression).expression)) {
type = narrowBySwitchOnTypeOf(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
}
return createFlowType(type, isIncomplete(flowType));
}

Expand Down Expand Up @@ -14634,6 +14654,83 @@ namespace ts {
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
}

function narrowBySwitchOnTypeOf(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): Type {
const switchWitnesses = getSwitchClauseTypeOfWitnesses(switchStatement);
if (!switchWitnesses.length) {
return type;
}
// Equal start and end denotes implicit fallthrough; undefined marks explicit default clause
const defaultCaseLocation = findIndex(switchWitnesses, elem => elem === undefined);
const hasDefaultClause = clauseStart === clauseEnd || (defaultCaseLocation >= clauseStart && defaultCaseLocation < clauseEnd);
let clauseWitnesses: string[];
let switchFacts: TypeFacts;
if (defaultCaseLocation > -1) {
// We no longer need the undefined denoting an
// explicit default case. Remove the undefined and
// fix-up clauseStart and clauseEnd. This means
// that we don't have to worry about undefined
// in the witness array.
const witnesses = <string[]>switchWitnesses.filter(witness => witness !== undefined);
// The adjust clause start and end after removing the `default` statement.
const fixedClauseStart = defaultCaseLocation < clauseStart ? clauseStart - 1 : clauseStart;
const fixedClauseEnd = defaultCaseLocation < clauseEnd ? clauseEnd - 1 : clauseEnd;
clauseWitnesses = witnesses.slice(fixedClauseStart, fixedClauseEnd);
switchFacts = getFactsFromTypeofSwitch(fixedClauseStart, fixedClauseEnd, witnesses, hasDefaultClause);
}
else {
clauseWitnesses = <string[]>switchWitnesses.slice(clauseStart, clauseEnd);
switchFacts = getFactsFromTypeofSwitch(clauseStart, clauseEnd, <string[]>switchWitnesses, hasDefaultClause);
}
/*
The implied type is the raw type suggested by a
value being caught in this clause.

When the clause contains a default case we ignore
the implied type and try to narrow using any facts
we can learn: see `switchFacts`.

Example:
switch (typeof x) {
case 'number':
case 'string': break;
default: break;
case 'number':
case 'boolean': break
}

In the first clause (case `number` and `string`) the
implied type is number | string.

In the default clause we de not compute an implied type.

In the third clause (case `number` and `boolean`)
the naive implied type is number | boolean, however
we use the type facts to narrow the implied type to
boolean. We know that number cannot be selected
because it is caught in the first clause.
*/
if (!(hasDefaultClause || (type.flags & TypeFlags.Union))) {
let impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => typeofTypesByName.get(text) || neverType)), switchFacts);
if (impliedType.flags & TypeFlags.Union) {
impliedType = getAssignmentReducedType(impliedType as UnionType, getBaseConstraintOfType(type) || type);
}
if (!(impliedType.flags & TypeFlags.Never)) {
if (isTypeSubtypeOf(impliedType, type)) {
return impliedType;
}
if (type.flags & TypeFlags.Instantiable) {
const constraint = getBaseConstraintOfType(type) || anyType;
if (isTypeSubtypeOf(impliedType, constraint)) {
return getIntersectionType([type, impliedType]);
}
}
}
}
return hasDefaultClause ?
filterType(type, t => (getTypeFacts(t) & switchFacts) === switchFacts) :
getTypeWithFacts(type, switchFacts);
}

function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
const left = getReferenceCandidate(expr.left);
if (!isMatchingReference(reference, left)) {
Expand Down Expand Up @@ -20337,10 +20434,62 @@ namespace ts {
: Diagnostics.Type_of_yield_operand_in_an_async_generator_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member);
}

/**
* Collect the TypeFacts learned from a typeof switch with
* total clauses `witnesses`, and the active clause ranging
* from `start` to `end`. Parameter `hasDefault` denotes
* whether the active clause contains a default clause.
*/
function getFactsFromTypeofSwitch(start: number, end: number, witnesses: string[], hasDefault: boolean): TypeFacts {
let facts: TypeFacts = TypeFacts.None;
// When in the default we only collect inequality facts
// because default is 'in theory' a set of infinite
// equalities.
if (hasDefault) {
// Value is not equal to any types after the active clause.
for (let i = end; i < witnesses.length; i++) {
facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject;
}
// Remove inequalities for types that appear in the
// active clause because they appear before other
// types collected so far.
for (let i = start; i < end; i++) {
facts &= ~(typeofNEFacts.get(witnesses[i]) || 0);
}
// Add inequalities for types before the active clause unconditionally.
for (let i = 0; i < start; i++) {
facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject;
}
}
// When in an active clause without default the set of
// equalities is finite.
else {
// Add equalities for all types in the active clause.
for (let i = start; i < end; i++) {
facts |= typeofEQFacts.get(witnesses[i]) || TypeFacts.TypeofEQHostObject;
}
// Remove equalities for types that appear before the
// active clause.
for (let i = 0; i < start; i++) {
facts &= ~(typeofEQFacts.get(witnesses[i]) || 0);
}
}
return facts;
}

function isExhaustiveSwitchStatement(node: SwitchStatement): boolean {
if (!node.possiblyExhaustive) {
return false;
}
if (node.expression.kind === SyntaxKind.TypeOfExpression) {
const operandType = getTypeOfExpression((node.expression as TypeOfExpression).expression);
// This cast is safe because the switch is possibly exhaustive and does not contain a default case, so there can be no undefined.
const witnesses = <string[]>getSwitchClauseTypeOfWitnesses(node);
// notEqualFacts states that the type of the switched value is not equal to every type in the switch.
const notEqualFacts = getFactsFromTypeofSwitch(0, 0, witnesses, /*hasDefault*/ true);
const type = getBaseConstraintOfType(operandType) || operandType;
return !!(filterType(type, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts).flags & TypeFlags.Never);
}
const type = getTypeOfExpression(node.expression);
if (!isLiteralType(type)) {
return false;
Expand Down
Loading