Skip to content

Commit 05d59a1

Browse files
Unify logic in typeof narrowing (#33434)
1 parent ab63176 commit 05d59a1

File tree

5 files changed

+249
-91
lines changed

5 files changed

+249
-91
lines changed

src/compiler/checker.ts

+19-36
Original file line numberDiff line numberDiff line change
@@ -20806,29 +20806,8 @@ namespace ts {
2080620806
const facts = assumeTrue ?
2080720807
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :
2080820808
typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject;
20809-
return getTypeWithFacts(assumeTrue ? mapType(type, narrowTypeForTypeof) : type, facts);
20810-
20811-
function narrowTypeForTypeof(type: Type) {
20812-
// We narrow a non-union type to an exact primitive type if the non-union type
20813-
// is a supertype of that primitive type. For example, type 'any' can be narrowed
20814-
// to one of the primitive types.
20815-
const targetType = literal.text === "function" ? globalFunctionType : typeofTypesByName.get(literal.text);
20816-
if (targetType) {
20817-
if (isTypeSubtypeOf(type, targetType)) {
20818-
return type;
20819-
}
20820-
if (isTypeSubtypeOf(targetType, type)) {
20821-
return targetType;
20822-
}
20823-
if (type.flags & TypeFlags.Instantiable) {
20824-
const constraint = getBaseConstraintOfType(type) || anyType;
20825-
if (isTypeSubtypeOf(targetType, constraint)) {
20826-
return getIntersectionType([type, targetType]);
20827-
}
20828-
}
20829-
}
20830-
return type;
20831-
}
20809+
const impliedType = getImpliedTypeFromTypeofGuard(type, literal.text);
20810+
return getTypeWithFacts(assumeTrue && impliedType ? mapType(type, narrowUnionMemberByTypeof(impliedType)) : type, facts);
2083220811
}
2083320812

2083420813
function narrowTypeBySwitchOptionalChainContainment(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number, clauseCheck: (type: Type) => boolean) {
@@ -20879,19 +20858,28 @@ namespace ts {
2087920858
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
2088020859
}
2088120860

20882-
function getImpliedTypeFromTypeofCase(type: Type, text: string) {
20861+
function getImpliedTypeFromTypeofGuard(type: Type, text: string) {
2088320862
switch (text) {
2088420863
case "function":
2088520864
return type.flags & TypeFlags.Any ? type : globalFunctionType;
2088620865
case "object":
2088720866
return type.flags & TypeFlags.Unknown ? getUnionType([nonPrimitiveType, nullType]) : type;
2088820867
default:
20889-
return typeofTypesByName.get(text) || type;
20868+
return typeofTypesByName.get(text);
2089020869
}
2089120870
}
2089220871

20893-
function narrowTypeForTypeofSwitch(candidate: Type) {
20872+
// When narrowing a union type by a `typeof` guard using type-facts alone, constituent types that are
20873+
// super-types of the implied guard will be retained in the final type: this is because type-facts only
20874+
// filter. Instead, we would like to replace those union constituents with the more precise type implied by
20875+
// the guard. For example: narrowing `{} | undefined` by `"boolean"` should produce the type `boolean`, not
20876+
// the filtered type `{}`. For this reason we narrow constituents of the union individually, in addition to
20877+
// filtering by type-facts.
20878+
function narrowUnionMemberByTypeof(candidate: Type) {
2089420879
return (type: Type) => {
20880+
if (isTypeSubtypeOf(type, candidate)) {
20881+
return type;
20882+
}
2089520883
if (isTypeSubtypeOf(candidate, type)) {
2089620884
return candidate;
2089720885
}
@@ -20916,11 +20904,9 @@ namespace ts {
2091620904
let clauseWitnesses: string[];
2091720905
let switchFacts: TypeFacts;
2091820906
if (defaultCaseLocation > -1) {
20919-
// We no longer need the undefined denoting an
20920-
// explicit default case. Remove the undefined and
20921-
// fix-up clauseStart and clauseEnd. This means
20922-
// that we don't have to worry about undefined
20923-
// in the witness array.
20907+
// We no longer need the undefined denoting an explicit default case. Remove the undefined and
20908+
// fix-up clauseStart and clauseEnd. This means that we don't have to worry about undefined in the
20909+
// witness array.
2092420910
const witnesses = <string[]>switchWitnesses.filter(witness => witness !== undefined);
2092520911
// The adjusted clause start and end after removing the `default` statement.
2092620912
const fixedClauseStart = defaultCaseLocation < clauseStart ? clauseStart - 1 : clauseStart;
@@ -20963,11 +20949,8 @@ namespace ts {
2096320949
boolean. We know that number cannot be selected
2096420950
because it is caught in the first clause.
2096520951
*/
20966-
let impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofCase(type, text))), switchFacts);
20967-
if (impliedType.flags & TypeFlags.Union) {
20968-
impliedType = getAssignmentReducedType(impliedType as UnionType, getBaseConstraintOrType(type));
20969-
}
20970-
return getTypeWithFacts(mapType(type, narrowTypeForTypeofSwitch(impliedType)), switchFacts);
20952+
const impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofGuard(type, text) || type)), switchFacts);
20953+
return getTypeWithFacts(mapType(type, narrowUnionMemberByTypeof(impliedType)), switchFacts);
2097120954
}
2097220955

2097320956
function isMatchingConstructorReference(expr: Expression) {

tests/baselines/reference/narrowingByTypeofInSwitch.js

+48-1
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,20 @@ function narrowingNarrows(x: {} | undefined) {
250250
}
251251
}
252252

253+
function narrowingNarrows2(x: true | 3 | 'hello' | undefined) {
254+
switch (typeof x) {
255+
case 'number': assertNumber(x); return;
256+
case 'boolean': assertBoolean(x); return;
257+
case 'function': assertNever(x); return;
258+
case 'symbol': assertNever(x); return;
259+
case 'object': const _: {} = assertNever(x); return;
260+
case 'string': assertString(x); return;
261+
case 'undefined': assertUndefined(x); return;
262+
case 'number': assertNever(x); return;
263+
default: const _y: {} = assertNever(x); return;
264+
}
265+
}
266+
253267
/* Template literals */
254268

255269
function testUnionWithTempalte(x: Basic) {
@@ -298,9 +312,11 @@ function multipleGenericFuseWithBoth<X extends L | number, Y extends R | number>
298312
case 'object': return [xy, 'two'];
299313
case `number`: return [xy]
300314
}
301-
}
315+
}
316+
302317

303318
//// [narrowingByTypeofInSwitch.js]
319+
"use strict";
304320
function assertNever(x) {
305321
return x;
306322
}
@@ -634,6 +650,37 @@ function narrowingNarrows(x) {
634650
return;
635651
}
636652
}
653+
function narrowingNarrows2(x) {
654+
switch (typeof x) {
655+
case 'number':
656+
assertNumber(x);
657+
return;
658+
case 'boolean':
659+
assertBoolean(x);
660+
return;
661+
case 'function':
662+
assertNever(x);
663+
return;
664+
case 'symbol':
665+
assertNever(x);
666+
return;
667+
case 'object':
668+
var _ = assertNever(x);
669+
return;
670+
case 'string':
671+
assertString(x);
672+
return;
673+
case 'undefined':
674+
assertUndefined(x);
675+
return;
676+
case 'number':
677+
assertNever(x);
678+
return;
679+
default:
680+
var _y = assertNever(x);
681+
return;
682+
}
683+
}
637684
/* Template literals */
638685
function testUnionWithTempalte(x) {
639686
switch (typeof x) {

0 commit comments

Comments
 (0)