Skip to content

Commit 01f865d

Browse files
committed
Fix instanceof operator narrowing issues
1 parent 269b828 commit 01f865d

File tree

1 file changed

+43
-10
lines changed

1 file changed

+43
-10
lines changed

src/compiler/checker.ts

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8084,6 +8084,25 @@ namespace ts {
80848084
return source.flags & TypeFlags.Union ? !forEach((<UnionType>source).types, t => !contains(types, t)) : contains(types, source);
80858085
}
80868086

8087+
function isTypeSubsetOf(source: Type, target: Type) {
8088+
return source === target || target.flags & TypeFlags.Union && isTypeSubsetOfUnion(source, <UnionType>target);
8089+
}
8090+
8091+
function isTypeSubsetOfUnion(source: Type, target: UnionType) {
8092+
if (source.flags & TypeFlags.Union) {
8093+
for (const t of (<UnionType>source).types) {
8094+
if (!containsType(target.types, t)) {
8095+
return false;
8096+
}
8097+
}
8098+
return true;
8099+
}
8100+
if (source.flags & TypeFlags.EnumLiteral && target.flags & TypeFlags.Enum && (<EnumLiteralType>source).baseType === target) {
8101+
return true;
8102+
}
8103+
return containsType(target.types, source);
8104+
}
8105+
80878106
function filterType(type: Type, f: (t: Type) => boolean): Type {
80888107
return type.flags & TypeFlags.Union ?
80898108
getUnionType(filter((<UnionType>type).types, f)) :
@@ -8230,6 +8249,7 @@ namespace ts {
82308249

82318250
function getTypeAtFlowBranchLabel(flow: FlowLabel): FlowType {
82328251
const antecedentTypes: Type[] = [];
8252+
let subtypeReduction = false;
82338253
let seenIncomplete = false;
82348254
for (const antecedent of flow.antecedents) {
82358255
const flowType = getTypeAtFlowNode(antecedent);
@@ -8244,11 +8264,17 @@ namespace ts {
82448264
if (!contains(antecedentTypes, type)) {
82458265
antecedentTypes.push(type);
82468266
}
8267+
// If an antecedent type is not a subset of the declared type, we need to perform
8268+
// subtype reduction. This happens when a "foreign" type is injected into the control
8269+
// flow using the instanceof operator or a user defined type predicate.
8270+
if (!isTypeSubsetOf(type, declaredType)) {
8271+
subtypeReduction = true;
8272+
}
82478273
if (isIncomplete(flowType)) {
82488274
seenIncomplete = true;
82498275
}
82508276
}
8251-
return createFlowType(getUnionType(antecedentTypes), seenIncomplete);
8277+
return createFlowType(getUnionType(antecedentTypes, subtypeReduction), seenIncomplete);
82528278
}
82538279

82548280
function getTypeAtFlowLoopLabel(flow: FlowLabel): FlowType {
@@ -8274,6 +8300,7 @@ namespace ts {
82748300
// Add the flow loop junction and reference to the in-process stack and analyze
82758301
// each antecedent code path.
82768302
const antecedentTypes: Type[] = [];
8303+
let subtypeReduction = false;
82778304
flowLoopNodes[flowLoopCount] = flow;
82788305
flowLoopKeys[flowLoopCount] = key;
82798306
flowLoopTypes[flowLoopCount] = antecedentTypes;
@@ -8290,14 +8317,20 @@ namespace ts {
82908317
if (!contains(antecedentTypes, type)) {
82918318
antecedentTypes.push(type);
82928319
}
8320+
// If an antecedent type is not a subset of the declared type, we need to perform
8321+
// subtype reduction. This happens when a "foreign" type is injected into the control
8322+
// flow using the instanceof operator or a user defined type predicate.
8323+
if (!isTypeSubsetOf(type, declaredType)) {
8324+
subtypeReduction = true;
8325+
}
82938326
// If the type at a particular antecedent path is the declared type there is no
82948327
// reason to process more antecedents since the only possible outcome is subtypes
82958328
// that will be removed in the final union type anyway.
82968329
if (type === declaredType) {
82978330
break;
82988331
}
82998332
}
8300-
return cache[key] = getUnionType(antecedentTypes);
8333+
return cache[key] = getUnionType(antecedentTypes, subtypeReduction);
83018334
}
83028335

83038336
function isMatchingPropertyAccess(expr: Expression) {
@@ -8494,9 +8527,7 @@ namespace ts {
84948527

84958528
function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean) {
84968529
if (!assumeTrue) {
8497-
return type.flags & TypeFlags.Union ?
8498-
getUnionType(filter((<UnionType>type).types, t => !isTypeSubtypeOf(t, candidate))) :
8499-
type;
8530+
return filterType(type, t => !isTypeSubtypeOf(t, candidate));
85008531
}
85018532
// If the current type is a union type, remove all constituents that aren't assignable to
85028533
// the candidate type. If one or more constituents remain, return a union of those.
@@ -8506,13 +8537,15 @@ namespace ts {
85068537
return getUnionType(assignableConstituents);
85078538
}
85088539
}
8509-
// If the candidate type is assignable to the target type, narrow to the candidate type.
8510-
// Otherwise, if the current type is assignable to the candidate, keep the current type.
8511-
// Otherwise, the types are completely unrelated, so narrow to the empty type.
8540+
// If the candidate type is a subtype of the target type, narrow to the candidate type.
8541+
// Otherwise, narrow to whichever of the target type or the candidate type that is assignable
8542+
// to the other. Otherwise, the types are completely unrelated, so narrow to an intersection
8543+
// of the two types.
85128544
const targetType = type.flags & TypeFlags.TypeParameter ? getApparentType(type) : type;
8513-
return isTypeAssignableTo(candidate, targetType) ? candidate :
8545+
return isTypeSubtypeOf(candidate, targetType) ? candidate :
85148546
isTypeAssignableTo(type, candidate) ? type :
8515-
getIntersectionType([type, candidate]);
8547+
isTypeAssignableTo(candidate, targetType) ? candidate :
8548+
getIntersectionType([type, candidate]);
85168549
}
85178550

85188551
function narrowTypeByTypePredicate(type: Type, callExpression: CallExpression, assumeTrue: boolean): Type {

0 commit comments

Comments
 (0)