Skip to content

Unify logic in typeof narrowing #33434

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

Merged
merged 2 commits into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
55 changes: 19 additions & 36 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20804,29 +20804,8 @@ namespace ts {
const facts = assumeTrue ?
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :
typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject;
return getTypeWithFacts(assumeTrue ? mapType(type, narrowTypeForTypeof) : type, facts);

function narrowTypeForTypeof(type: Type) {
// We narrow a non-union type to an exact primitive type if the non-union type
// is a supertype of that primitive type. For example, type 'any' can be narrowed
// to one of the primitive types.
const targetType = literal.text === "function" ? globalFunctionType : typeofTypesByName.get(literal.text);
if (targetType) {
if (isTypeSubtypeOf(type, targetType)) {
return type;
}
if (isTypeSubtypeOf(targetType, type)) {
return targetType;
}
if (type.flags & TypeFlags.Instantiable) {
const constraint = getBaseConstraintOfType(type) || anyType;
if (isTypeSubtypeOf(targetType, constraint)) {
return getIntersectionType([type, targetType]);
}
}
}
return type;
}
const impliedType = getImpliedTypeFromTypeofGuard(type, literal.text);
return getTypeWithFacts(assumeTrue && impliedType ? mapType(type, narrowUnionMemberByTypeof(impliedType)) : type, facts);
}

function narrowTypeBySwitchOptionalChainContainment(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number, clauseCheck: (type: Type) => boolean) {
Expand Down Expand Up @@ -20877,19 +20856,28 @@ namespace ts {
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
}

function getImpliedTypeFromTypeofCase(type: Type, text: string) {
function getImpliedTypeFromTypeofGuard(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;
return typeofTypesByName.get(text);
}
}

function narrowTypeForTypeofSwitch(candidate: Type) {
// When narrowing a union type by a `typeof` guard using type-facts alone, constituent types that are
// super-types of the implied guard will be retained in the final type: this is because type-facts only
// filter. Instead, we would like to replace those union constituents with the more precise type implied by
// the guard. For example: narrowing `{} | undefined` by `"boolean"` should produce the type `boolean`, not
// the filtered type `{}`. For this reason we narrow constituents of the union individually, in addition to
// filtering by type-facts.
function narrowUnionMemberByTypeof(candidate: Type) {
return (type: Type) => {
if (isTypeSubtypeOf(type, candidate)) {
return type;
}
if (isTypeSubtypeOf(candidate, type)) {
return candidate;
}
Expand All @@ -20914,11 +20902,9 @@ namespace ts {
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.
// 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 adjusted clause start and end after removing the `default` statement.
const fixedClauseStart = defaultCaseLocation < clauseStart ? clauseStart - 1 : clauseStart;
Expand Down Expand Up @@ -20961,11 +20947,8 @@ namespace ts {
boolean. We know that number cannot be selected
because it is caught in the first clause.
*/
let impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofCase(type, text))), switchFacts);
if (impliedType.flags & TypeFlags.Union) {
impliedType = getAssignmentReducedType(impliedType as UnionType, getBaseConstraintOrType(type));
}
return getTypeWithFacts(mapType(type, narrowTypeForTypeofSwitch(impliedType)), switchFacts);
const impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofGuard(type, text) || type)), switchFacts);
return getTypeWithFacts(mapType(type, narrowUnionMemberByTypeof(impliedType)), switchFacts);
}

function isMatchingConstructorReference(expr: Expression) {
Expand Down
49 changes: 48 additions & 1 deletion tests/baselines/reference/narrowingByTypeofInSwitch.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,20 @@ function narrowingNarrows(x: {} | undefined) {
}
}

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

/* Template literals */

function testUnionWithTempalte(x: Basic) {
Expand Down Expand Up @@ -298,9 +312,11 @@ function multipleGenericFuseWithBoth<X extends L | number, Y extends R | number>
case 'object': return [xy, 'two'];
case `number`: return [xy]
}
}
}


//// [narrowingByTypeofInSwitch.js]
"use strict";
function assertNever(x) {
return x;
}
Expand Down Expand Up @@ -634,6 +650,37 @@ function narrowingNarrows(x) {
return;
}
}
function narrowingNarrows2(x) {
switch (typeof x) {
case 'number':
assertNumber(x);
return;
case 'boolean':
assertBoolean(x);
return;
case 'function':
assertNever(x);
return;
case 'symbol':
assertNever(x);
return;
case 'object':
var _ = assertNever(x);
return;
case 'string':
assertString(x);
return;
case 'undefined':
assertUndefined(x);
return;
case 'number':
assertNever(x);
return;
default:
var _y = assertNever(x);
return;
}
}
/* Template literals */
function testUnionWithTempalte(x) {
switch (typeof x) {
Expand Down
Loading