Skip to content

Bring typeof switch behaviour inline with if #27182

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

54 changes: 34 additions & 20 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15215,6 +15215,32 @@ namespace ts {
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
}

function getTypeFromName(type: Type, text: string) {
switch (text) {
case "function":
return type.flags & TypeFlags.Any ? type : globalFunctionType;
case "object":
return type.flags & TypeFlags.Unknown ? getUnionType([nonPrimitiveType, nullType]) : type;
default:
return typeofTypesByName.get(text) || type;
}
}

function narrowTypeForTypeofSwitch(candidate: Type) {
return (type: Type) => {
if (isTypeSubtypeOf(candidate, type)) {
return candidate;
}
if (type.flags & TypeFlags.Instantiable) {
const constraint = getBaseConstraintOfType(type) || anyType;
if (isTypeSubtypeOf(candidate, constraint)) {
return getIntersectionType([type, candidate]);
}
}
return type;
};
}

function narrowBySwitchOnTypeOf(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): Type {
const switchWitnesses = getSwitchClauseTypeOfWitnesses(switchStatement);
if (!switchWitnesses.length) {
Expand All @@ -15232,7 +15258,7 @@ namespace ts {
// 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.
// The adjusted 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);
Expand All @@ -15242,6 +15268,9 @@ namespace ts {
clauseWitnesses = <string[]>switchWitnesses.slice(clauseStart, clauseEnd);
switchFacts = getFactsFromTypeofSwitch(clauseStart, clauseEnd, <string[]>switchWitnesses, hasDefaultClause);
}
if (hasDefaultClause) {
return filterType(type, t => (getTypeFacts(t) & switchFacts) === switchFacts);
}
/*
The implied type is the raw type suggested by a
value being caught in this clause.
Expand Down Expand Up @@ -15270,26 +15299,11 @@ namespace ts {
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]);
}
}
}
let impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getTypeFromName(type, text))), switchFacts);
if (impliedType.flags & TypeFlags.Union) {
impliedType = getAssignmentReducedType(impliedType as UnionType, getBaseConstraintOrType(type));
}
return hasDefaultClause ?
filterType(type, t => (getTypeFacts(t) & switchFacts) === switchFacts) :
getTypeWithFacts(type, switchFacts);
return getTypeWithFacts(mapType(type, narrowTypeForTypeofSwitch(impliedType)), switchFacts);
}

function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
Expand Down
119 changes: 117 additions & 2 deletions tests/baselines/reference/narrowingByTypeofInSwitch.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ function assertObject(x: object) {
return x;
}

function assertObjectOrNull(x: object | null) {
return x;
}

function assertUndefined(x: undefined) {
return x;
}
Expand All @@ -35,11 +39,11 @@ function assertAll(x: Basic) {
return x;
}

function assertStringOrNumber(x: string | number) {
function assertStringOrNumber(x: string | number) {
return x;
}

function assertBooleanOrObject(x: boolean | object) {
function assertBooleanOrObject(x: boolean | object) {
return x;
}

Expand Down Expand Up @@ -210,6 +214,41 @@ function fallThroughTest(x: string | number | boolean | object) {
break;
}
}

function unknownNarrowing(x: unknown) {
switch (typeof x) {
case 'number': assertNumber(x); return;
case 'boolean': assertBoolean(x); return;
case 'function': assertFunction(x); return;
case 'symbol': assertSymbol(x); return;
case 'object': assertObjectOrNull(x); return;
case 'string': assertString(x); return;
case 'undefined': assertUndefined(x); return;
}
}

function keyofNarrowing<S extends { [K in keyof S]: string }>(k: keyof S) {
function assertKeyofS(k1: keyof S) { }
switch (typeof k) {
case 'number': assertNumber(k); assertKeyofS(k); return;
case 'symbol': assertSymbol(k); assertKeyofS(k); return;
case 'string': assertString(k); assertKeyofS(k); return;
}
}

function narrowingNarrows(x: {} | undefined) {
switch (typeof x) {
case 'number': assertNumber(x); return;
case 'boolean': assertBoolean(x); return;
case 'function': assertFunction(x); return;
case 'symbol': assertSymbol(x); return;
case 'object': const _: {} = x; return;
case 'string': assertString(x); return;
case 'undefined': assertUndefined(x); return;
case 'number': assertNever(x); return;
default: const _y: {} = x; return;
}
}


//// [narrowingByTypeofInSwitch.js]
Expand All @@ -234,6 +273,9 @@ function assertFunction(x) {
function assertObject(x) {
return x;
}
function assertObjectOrNull(x) {
return x;
}
function assertUndefined(x) {
return x;
}
Expand Down Expand Up @@ -470,3 +512,76 @@ function fallThroughTest(x) {
break;
}
}
function unknownNarrowing(x) {
switch (typeof x) {
case 'number':
assertNumber(x);
return;
case 'boolean':
assertBoolean(x);
return;
case 'function':
assertFunction(x);
return;
case 'symbol':
assertSymbol(x);
return;
case 'object':
assertObjectOrNull(x);
return;
case 'string':
assertString(x);
return;
case 'undefined':
assertUndefined(x);
return;
}
}
function keyofNarrowing(k) {
function assertKeyofS(k1) { }
switch (typeof k) {
case 'number':
assertNumber(k);
assertKeyofS(k);
return;
case 'symbol':
assertSymbol(k);
assertKeyofS(k);
return;
case 'string':
assertString(k);
assertKeyofS(k);
return;
}
}
function narrowingNarrows(x) {
switch (typeof x) {
case 'number':
assertNumber(x);
return;
case 'boolean':
assertBoolean(x);
return;
case 'function':
assertFunction(x);
return;
case 'symbol':
assertSymbol(x);
return;
case 'object':
var _ = x;
return;
case 'string':
assertString(x);
return;
case 'undefined':
assertUndefined(x);
return;
case 'number':
assertNever(x);
return;
default:
var _y = x;
return;
}
}
Loading