Skip to content

Commit 8be155d

Browse files
committed
Unify logic in typeof narrowing
1 parent 038d951 commit 8be155d

File tree

1 file changed

+17
-38
lines changed

1 file changed

+17
-38
lines changed

src/compiler/checker.ts

+17-38
Original file line numberDiff line numberDiff line change
@@ -17511,32 +17511,7 @@ namespace ts {
1751117511
const facts = assumeTrue ?
1751217512
typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject :
1751317513
typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject;
17514-
return getTypeWithFacts(assumeTrue ? mapType(type, narrowTypeForTypeof) : type, facts);
17515-
17516-
function narrowTypeForTypeof(type: Type) {
17517-
if (type.flags & TypeFlags.Unknown && literal.text === "object") {
17518-
return getUnionType([nonPrimitiveType, nullType]);
17519-
}
17520-
// We narrow a non-union type to an exact primitive type if the non-union type
17521-
// is a supertype of that primitive type. For example, type 'any' can be narrowed
17522-
// to one of the primitive types.
17523-
const targetType = literal.text === "function" ? globalFunctionType : typeofTypesByName.get(literal.text);
17524-
if (targetType) {
17525-
if (isTypeSubtypeOf(type, targetType)) {
17526-
return type;
17527-
}
17528-
if (isTypeSubtypeOf(targetType, type)) {
17529-
return targetType;
17530-
}
17531-
if (type.flags & TypeFlags.Instantiable) {
17532-
const constraint = getBaseConstraintOfType(type) || anyType;
17533-
if (isTypeSubtypeOf(targetType, constraint)) {
17534-
return getIntersectionType([type, targetType]);
17535-
}
17536-
}
17537-
}
17538-
return type;
17539-
}
17514+
return getTypeWithFacts(assumeTrue ? mapType(type, narrowUnionMemberByTypeof(getImpliedTypeFromTypeofGuard(type, literal.text))) : type, facts);
1754017515
}
1754117516

1754217517
function narrowTypeBySwitchOnDiscriminant(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) {
@@ -17582,7 +17557,7 @@ namespace ts {
1758217557
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
1758317558
}
1758417559

17585-
function getImpliedTypeFromTypeofCase(type: Type, text: string) {
17560+
function getImpliedTypeFromTypeofGuard(type: Type, text: string) {
1758617561
switch (text) {
1758717562
case "function":
1758817563
return type.flags & TypeFlags.Any ? type : globalFunctionType;
@@ -17593,8 +17568,17 @@ namespace ts {
1759317568
}
1759417569
}
1759517570

17596-
function narrowTypeForTypeofSwitch(candidate: Type) {
17571+
// When narrowing a union type by a `typeof` guard using type-facts alone, constituent types that are
17572+
// super-types of the implied guard will be retained in the final type: this is because type-facts only
17573+
// filter. Instead, we would like to replace those union constituents with the more precise type implied by
17574+
// the guard. For example: narrowing `{} | undefined` by `"boolean"` should produce the type `boolean`, not
17575+
// the filtered type `{}`. For this reason we narrow constituents of the union individually, in addition to
17576+
// filtering by type-facts.
17577+
function narrowUnionMemberByTypeof(candidate: Type) {
1759717578
return (type: Type) => {
17579+
if (isTypeSubtypeOf(type, candidate)) {
17580+
return type;
17581+
}
1759817582
if (isTypeSubtypeOf(candidate, type)) {
1759917583
return candidate;
1760017584
}
@@ -17619,11 +17603,9 @@ namespace ts {
1761917603
let clauseWitnesses: string[];
1762017604
let switchFacts: TypeFacts;
1762117605
if (defaultCaseLocation > -1) {
17622-
// We no longer need the undefined denoting an
17623-
// explicit default case. Remove the undefined and
17624-
// fix-up clauseStart and clauseEnd. This means
17625-
// that we don't have to worry about undefined
17626-
// in the witness array.
17606+
// We no longer need the undefined denoting an explicit default case. Remove the undefined and
17607+
// fix-up clauseStart and clauseEnd. This means that we don't have to worry about undefined in the
17608+
// witness array.
1762717609
const witnesses = <string[]>switchWitnesses.filter(witness => witness !== undefined);
1762817610
// The adjusted clause start and end after removing the `default` statement.
1762917611
const fixedClauseStart = defaultCaseLocation < clauseStart ? clauseStart - 1 : clauseStart;
@@ -17666,11 +17648,8 @@ namespace ts {
1766617648
boolean. We know that number cannot be selected
1766717649
because it is caught in the first clause.
1766817650
*/
17669-
let impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofCase(type, text))), switchFacts);
17670-
if (impliedType.flags & TypeFlags.Union) {
17671-
impliedType = getAssignmentReducedType(impliedType as UnionType, getBaseConstraintOrType(type));
17672-
}
17673-
return getTypeWithFacts(mapType(type, narrowTypeForTypeofSwitch(impliedType)), switchFacts);
17651+
const impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofGuard(type, text))), switchFacts);
17652+
return getTypeWithFacts(mapType(type, narrowUnionMemberByTypeof(impliedType)), switchFacts);
1767417653
}
1767517654

1767617655
function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {

0 commit comments

Comments
 (0)