Skip to content

Commit f0b8742

Browse files
committed
Only cache union/intersection relations once
1 parent b82966f commit f0b8742

File tree

1 file changed

+68
-71
lines changed

1 file changed

+68
-71
lines changed

src/compiler/checker.ts

Lines changed: 68 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -18346,30 +18346,17 @@ namespace ts {
1834618346
let result = Ternary.False;
1834718347
const saveErrorInfo = captureErrorCalculationState();
1834818348

18349-
if (source.flags & TypeFlags.UnionOrIntersection || target.flags & TypeFlags.UnionOrIntersection) {
18350-
// We skip caching when source or target is a union with no more than three constituents.
18351-
result = (source.flags & TypeFlags.Union || target.flags & TypeFlags.Union) && getConstituentCount(source) * getConstituentCount(target) < 4 ?
18352-
structuredTypeRelatedTo(source, target, reportErrors, intersectionState | IntersectionState.UnionIntersectionCheck) :
18353-
recursiveTypeRelatedTo(source, target, reportErrors, intersectionState | IntersectionState.UnionIntersectionCheck, recursionFlags);
18354-
// The ordered decomposition above doesn't handle all cases. Specifically, we also need to handle:
18355-
// Source is instantiable (e.g. source has union or intersection constraint).
18356-
// Source is an object, target is a union (e.g. { a, b: boolean } <=> { a, b: true } | { a, b: false }).
18357-
// Source is an intersection, target is an object (e.g. { a } & { b } <=> { a, b }).
18358-
// Source is an intersection, target is a union (e.g. { a } & { b: boolean } <=> { a, b: true } | { a, b: false }).
18359-
// Source is an intersection, target instantiable (e.g. string & { tag } <=> T["a"] constrained to string & { tag }).
18360-
if (!result && (source.flags & TypeFlags.Instantiable ||
18361-
source.flags & TypeFlags.Object && target.flags & TypeFlags.Union ||
18362-
source.flags & TypeFlags.Intersection && target.flags & (TypeFlags.Object | TypeFlags.Union | TypeFlags.Instantiable))) {
18363-
if (result = recursiveTypeRelatedTo(source, target, reportErrors, intersectionState, recursionFlags)) {
18364-
resetErrorInfo(saveErrorInfo);
18365-
}
18349+
if (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable) {
18350+
const skipCaching = source.flags & TypeFlags.Union && (source as UnionType).types.length < 4 && !(target.flags & TypeFlags.Union) ||
18351+
target.flags & TypeFlags.Union && (target as UnionType).types.length < 4 && !(source.flags & TypeFlags.StructuredOrInstantiable);
18352+
if (skipCaching) {
18353+
result = unionOrIntersectionRelatedTo(source, target, reportErrors, intersectionState);
1836618354
}
18367-
}
18368-
else if (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable) {
18369-
if (result = recursiveTypeRelatedTo(source, target, reportErrors, intersectionState, recursionFlags)) {
18355+
else if (result = recursiveTypeRelatedTo(source, target, reportErrors, intersectionState, recursionFlags)) {
1837018356
resetErrorInfo(saveErrorInfo);
1837118357
}
1837218358
}
18359+
1837318360
if (!result && source.flags & (TypeFlags.Intersection | TypeFlags.TypeParameter)) {
1837418361
// The combined constraint of an intersection type is the intersection of the constraints of
1837518362
// the constituents. When an intersection type contains instantiable types with union type
@@ -18606,6 +18593,51 @@ namespace ts {
1860618593
return prop.valueDeclaration && container.valueDeclaration && prop.valueDeclaration.parent === container.valueDeclaration;
1860718594
}
1860818595

18596+
function unionOrIntersectionRelatedTo(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary {
18597+
// Note that these checks are specifically ordered to produce correct results. In particular,
18598+
// we need to deconstruct unions before intersections (because unions are always at the top),
18599+
// and we need to handle "each" relations before "some" relations for the same kind of type.
18600+
if (source.flags & TypeFlags.Union) {
18601+
return relation === comparableRelation ?
18602+
someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState & ~IntersectionState.UnionIntersectionCheck) :
18603+
eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState & ~IntersectionState.UnionIntersectionCheck);
18604+
}
18605+
if (target.flags & TypeFlags.Union) {
18606+
return typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), target as UnionType, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive));
18607+
}
18608+
if (target.flags & TypeFlags.Intersection) {
18609+
return typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as IntersectionType, reportErrors, IntersectionState.Target);
18610+
}
18611+
// Source is an intersection. For the comparable relation, if the target is a primitive type we hoist the
18612+
// constraints of all non-primitive types in the source into a new intersection. We do this because the
18613+
// intersection may further constrain the constraints of the non-primitive types. For example, given a type
18614+
// parameter 'T extends 1 | 2', the intersection 'T & 1' should be reduced to '1' such that it doesn't
18615+
// appear to be comparable to '2'.
18616+
if (relation === comparableRelation && target.flags & TypeFlags.Primitive) {
18617+
const constraints = sameMap((source as IntersectionType).types, getBaseConstraintOrType);
18618+
if (constraints !== (source as IntersectionType).types) {
18619+
source = getIntersectionType(constraints);
18620+
if (!(source.flags & TypeFlags.Intersection)) {
18621+
return isRelatedTo(source, target, RecursionFlags.Source, /*reportErrors*/ false);
18622+
}
18623+
}
18624+
}
18625+
// Check to see if any constituents of the intersection are immediately related to the target.
18626+
//
18627+
// Don't report errors though. Checking whether a constituent is related to the source is not actually
18628+
// useful and leads to some confusing error messages. Instead it is better to let the below checks
18629+
// take care of this, or to not elaborate at all. For instance,
18630+
//
18631+
// - For an object type (such as 'C = A & B'), users are usually more interested in structural errors.
18632+
//
18633+
// - For a union type (such as '(A | B) = (C & D)'), it's better to hold onto the whole intersection
18634+
// than to report that 'D' is not assignable to 'A' or 'B'.
18635+
//
18636+
// - For a primitive type or type parameter (such as 'number = A & B') there is no point in
18637+
// breaking the intersection apart.
18638+
return someTypeRelatedToType(source as IntersectionType, target, /*reportErrors*/ false, IntersectionState.Source);
18639+
}
18640+
1860918641
function eachTypeRelatedToSomeType(source: UnionOrIntersectionType, target: UnionOrIntersectionType): Ternary {
1861018642
let result = Ternary.True;
1861118643
const sourceTypes = source.types;
@@ -18903,49 +18935,23 @@ namespace ts {
1890318935
if (intersectionState & IntersectionState.PropertyCheck) {
1890418936
return propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None);
1890518937
}
18906-
if (intersectionState & IntersectionState.UnionIntersectionCheck) {
18907-
// Note that these checks are specifically ordered to produce correct results. In particular,
18908-
// we need to deconstruct unions before intersections (because unions are always at the top),
18909-
// and we need to handle "each" relations before "some" relations for the same kind of type.
18910-
if (source.flags & TypeFlags.Union) {
18911-
return relation === comparableRelation ?
18912-
someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState & ~IntersectionState.UnionIntersectionCheck) :
18913-
eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState & ~IntersectionState.UnionIntersectionCheck);
18914-
}
18915-
if (target.flags & TypeFlags.Union) {
18916-
return typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), target as UnionType, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive));
18917-
}
18918-
if (target.flags & TypeFlags.Intersection) {
18919-
return typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as IntersectionType, reportErrors, IntersectionState.Target);
18920-
}
18921-
// Source is an intersection. For the comparable relation, if the target is a primitive type we hoist the
18922-
// constraints of all non-primitive types in the source into a new intersection. We do this because the
18923-
// intersection may further constrain the constraints of the non-primitive types. For example, given a type
18924-
// parameter 'T extends 1 | 2', the intersection 'T & 1' should be reduced to '1' such that it doesn't
18925-
// appear to be comparable to '2'.
18926-
if (relation === comparableRelation && target.flags & TypeFlags.Primitive) {
18927-
const constraints = sameMap((source as IntersectionType).types, getBaseConstraintOrType);
18928-
if (constraints !== (source as IntersectionType).types) {
18929-
source = getIntersectionType(constraints);
18930-
if (!(source.flags & TypeFlags.Intersection)) {
18931-
return isRelatedTo(source, target, RecursionFlags.Source, /*reportErrors*/ false);
18932-
}
18933-
}
18938+
let result: Ternary;
18939+
let originalErrorInfo: DiagnosticMessageChain | undefined;
18940+
let varianceCheckFailed = false;
18941+
const saveErrorInfo = captureErrorCalculationState();
18942+
if (source.flags & TypeFlags.UnionOrIntersection || target.flags & TypeFlags.UnionOrIntersection) {
18943+
result = unionOrIntersectionRelatedTo(source, target, reportErrors, intersectionState);
18944+
// The ordered decomposition above doesn't handle all cases. Specifically, we also need to handle:
18945+
// Source is instantiable (e.g. source has union or intersection constraint).
18946+
// Source is an object, target is a union (e.g. { a, b: boolean } <=> { a, b: true } | { a, b: false }).
18947+
// Source is an intersection, target is an object (e.g. { a } & { b } <=> { a, b }).
18948+
// Source is an intersection, target is a union (e.g. { a } & { b: boolean } <=> { a, b: true } | { a, b: false }).
18949+
// Source is an intersection, target instantiable (e.g. string & { tag } <=> T["a"] constrained to string & { tag }).
18950+
if (result || !(source.flags & TypeFlags.Instantiable ||
18951+
source.flags & TypeFlags.Object && target.flags & TypeFlags.Union ||
18952+
source.flags & TypeFlags.Intersection && target.flags & (TypeFlags.Object | TypeFlags.Union | TypeFlags.Instantiable))) {
18953+
return result;
1893418954
}
18935-
// Check to see if any constituents of the intersection are immediately related to the target.
18936-
//
18937-
// Don't report errors though. Checking whether a constituent is related to the source is not actually
18938-
// useful and leads to some confusing error messages. Instead it is better to let the below checks
18939-
// take care of this, or to not elaborate at all. For instance,
18940-
//
18941-
// - For an object type (such as 'C = A & B'), users are usually more interested in structural errors.
18942-
//
18943-
// - For a union type (such as '(A | B) = (C & D)'), it's better to hold onto the whole intersection
18944-
// than to report that 'D' is not assignable to 'A' or 'B'.
18945-
//
18946-
// - For a primitive type or type parameter (such as 'number = A & B') there is no point in
18947-
// breaking the intersection apart.
18948-
return someTypeRelatedToType(source as IntersectionType, target, /*reportErrors*/ false, IntersectionState.Source);
1894918955
}
1895018956
const flags = source.flags & target.flags;
1895118957
if (relation === identityRelation && !(flags & TypeFlags.Object)) {
@@ -18979,11 +18985,6 @@ namespace ts {
1897918985
return Ternary.False;
1898018986
}
1898118987

18982-
let result: Ternary;
18983-
let originalErrorInfo: DiagnosticMessageChain | undefined;
18984-
let varianceCheckFailed = false;
18985-
const saveErrorInfo = captureErrorCalculationState();
18986-
1898718988
// We limit alias variance probing to only object and conditional types since their alias behavior
1898818989
// is more predictable than other, interned types, which may or may not have an alias depending on
1898918990
// the order in which things were checked.
@@ -23338,10 +23339,6 @@ namespace ts {
2333823339
mapType(type, mapper);
2333923340
}
2334023341

23341-
function getConstituentCount(type: Type) {
23342-
return type.flags & TypeFlags.Union ? (type as UnionType).types.length : 1;
23343-
}
23344-
2334523342
function extractTypesOfKind(type: Type, kind: TypeFlags) {
2334623343
return filterType(type, t => (t.flags & kind) !== 0);
2334723344
}

0 commit comments

Comments
 (0)