@@ -1381,6 +1381,8 @@ export function isInstantiatedModule(node: ModuleDeclaration, preserveConstEnums
1381
1381
(preserveConstEnums && moduleState === ModuleInstanceState.ConstEnumOnly);
1382
1382
}
1383
1383
1384
+ const UNION_CROSS_PRODUCT_SIZE_LIMIT = 100_000;
1385
+
1384
1386
/** @internal */
1385
1387
export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1386
1388
// Why var? It avoids TDZ checks in the runtime which can be costly.
@@ -11451,7 +11453,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
11451
11453
if (!links.type) {
11452
11454
Debug.assertIsDefined(links.deferralParent);
11453
11455
Debug.assertIsDefined(links.deferralConstituents);
11454
- links.type = links.deferralParent.flags & TypeFlags.Union ? getUnionType(links.deferralConstituents) : getIntersectionType(links.deferralConstituents);
11456
+ const operation = links.deferralParent.flags & TypeFlags.Union ? getUnionType : getIntersectionType;
11457
+ let result = operation(links.deferralConstituents);
11458
+ for (const part of links.deferredMismatchedParts || emptyArray) {
11459
+ result = operation([result, getTypeOfSymbol(part)]);
11460
+ }
11461
+ links.type = result;
11455
11462
}
11456
11463
return links.type;
11457
11464
}
@@ -11460,8 +11467,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
11460
11467
const links = getSymbolLinks(symbol);
11461
11468
if (!links.writeType && links.deferralWriteConstituents) {
11462
11469
Debug.assertIsDefined(links.deferralParent);
11463
- Debug.assertIsDefined(links.deferralConstituents);
11464
- links.writeType = links.deferralParent.flags & TypeFlags.Union ? getUnionType(links.deferralWriteConstituents) : getIntersectionType(links.deferralWriteConstituents);
11470
+ const operation = links.deferralParent.flags & TypeFlags.Union ? getUnionType : getIntersectionType;
11471
+ let result = operation(links.deferralWriteConstituents);
11472
+ for (const part of links.deferredMismatchedParts || emptyArray) {
11473
+ result = operation([result, getTypeOfSymbol(part)]);
11474
+ }
11475
+ links.writeType = result;
11465
11476
}
11466
11477
return links.writeType;
11467
11478
}
@@ -14035,8 +14046,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
14035
14046
let declarations: Declaration[] | undefined;
14036
14047
let firstType: Type | undefined;
14037
14048
let nameType: Type | undefined;
14038
- const propTypes: Type[] = [];
14049
+ let propTypes: Type[] = [];
14039
14050
let writeTypes: Type[] | undefined;
14051
+ let deferredMismatchedParts: Symbol[] | undefined;
14040
14052
let firstValueDeclaration: Declaration | undefined;
14041
14053
let hasNonUniformValueDeclaration = false;
14042
14054
for (const prop of props) {
@@ -14047,6 +14059,27 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
14047
14059
hasNonUniformValueDeclaration = true;
14048
14060
}
14049
14061
declarations = addRange(declarations, prop.declarations);
14062
+ if (getCheckFlags(prop) & CheckFlags.DeferredType) {
14063
+ checkFlags |= (getCheckFlags(prop) & (CheckFlags.HasNeverType | CheckFlags.HasNonUniformType | CheckFlags.HasLiteralType));
14064
+ if (!nameType) {
14065
+ nameType = getSymbolLinks(prop).nameType;
14066
+ }
14067
+ if ((getSymbolLinks(prop).deferralParent?.flags! & TypeFlags.UnionOrIntersection) === (containingType.flags & TypeFlags.UnionOrIntersection)) {
14068
+ // Member has a deferred type (of the same kind) - rather than eagerly resolving it, pass on the deferral
14069
+ const deferredWriteTypes = getSymbolLinks(prop).deferralWriteConstituents;
14070
+ if (deferredWriteTypes) {
14071
+ writeTypes = concatenate(!writeTypes ? propTypes.slice() : writeTypes, deferredWriteTypes);
14072
+ }
14073
+ propTypes = concatenate(propTypes, getSymbolLinks(prop).deferralConstituents!);
14074
+ deferredMismatchedParts = concatenate(deferredMismatchedParts, getSymbolLinks(prop).deferredMismatchedParts);
14075
+ break;
14076
+ }
14077
+ else {
14078
+ // deferred union used in an intersection or intersection used within an union - defer the whole construct
14079
+ deferredMismatchedParts = append(deferredMismatchedParts, prop);
14080
+ break;
14081
+ }
14082
+ }
14050
14083
const type = getTypeOfSymbol(prop);
14051
14084
if (!firstType) {
14052
14085
firstType = type;
@@ -14081,12 +14114,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
14081
14114
14082
14115
result.declarations = declarations;
14083
14116
result.links.nameType = nameType;
14084
- if (propTypes.length > 2) {
14117
+ if (propTypes.length > 2 || deferredMismatchedParts ) {
14085
14118
// When `propTypes` has the potential to explode in size when normalized, defer normalization until absolutely needed
14086
14119
result.links.checkFlags |= CheckFlags.DeferredType;
14087
14120
result.links.deferralParent = containingType;
14088
14121
result.links.deferralConstituents = propTypes;
14089
14122
result.links.deferralWriteConstituents = writeTypes;
14123
+ result.links.deferredMismatchedParts = deferredMismatchedParts;
14090
14124
}
14091
14125
else {
14092
14126
result.links.type = isUnion ? getUnionType(propTypes) : getIntersectionType(propTypes);
@@ -16723,7 +16757,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
16723
16757
function getIntersectionType(types: readonly Type[], aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[], noSupertypeReduction?: boolean): Type {
16724
16758
const typeMembershipMap: Map<string, Type> = new Map();
16725
16759
const includes = addTypesToIntersection(typeMembershipMap, 0 as TypeFlags, types);
16726
- const typeSet: Type[] = arrayFrom(typeMembershipMap.values());
16760
+ let typeSet: Type[] = arrayFrom(typeMembershipMap.values());
16727
16761
// An intersection type is considered empty if it contains
16728
16762
// the type never, or
16729
16763
// more than one unit type or,
@@ -16774,6 +16808,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
16774
16808
const id = getTypeListId(typeSet) + getAliasId(aliasSymbol, aliasTypeArguments);
16775
16809
let result = intersectionTypes.get(id);
16776
16810
if (!result) {
16811
+ const originalSet = typeSet;
16812
+ let runningResult: Type | undefined;
16777
16813
if (includes & TypeFlags.Union) {
16778
16814
if (intersectUnionsOfPrimitiveTypes(typeSet)) {
16779
16815
// When the intersection creates a reduced set (which might mean that *all* union types have
@@ -16791,18 +16827,56 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
16791
16827
result = getUnionType([getIntersectionType(typeSet), nullType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
16792
16828
}
16793
16829
else {
16830
+ if (typeSet.length > 2 && getCrossProductUnionSize(typeSet) >= UNION_CROSS_PRODUCT_SIZE_LIMIT && every(typeSet, t => !!(t.flags & TypeFlags.Union) || !!(t.flags & TypeFlags.Primitive))) {
16831
+ // This type set is going to trigger an "expression too complex" error below. Rather than resort to that, as a last, best effort, simplify the type.
16832
+ // When the intersection looks like (A | B | C) & (D | E | F) & (G | H | I) - in the general case, this can result in a massive resulting
16833
+ // union, hence the check on the cross product size below, _however_ in some cases we can also _simplify_ the resulting type massively.
16834
+ // If we can recognize that upfront, we can still allow the type to form without creating innumerable intermediate types.
16835
+ // Specifically, in cases where almost all combinations are known to reduce to `never` (so the result is essentially sparse)
16836
+ // and we can recognize that quickly, we can use a simplified result without checking the worst-case size.
16837
+ // So we start with the assumption that the result _is_ sparse when the input looks like the above, and we assume the result
16838
+ // will take the form (A & D & G) | (B & E & H) | (C & F & I). To validate this, we reduce left, first combining
16839
+ // (A | B | C) & (D | E | F); if that combines into `(A & D) | (B & E) | (C & F)` like we want, which we make 9 intermediate
16840
+ // types to check, we can then combine the reduced `(A & D) | (B & E) | (C & F)` with (G | H | I), which again takes 9 intermediate types
16841
+ // to check, finally producing `(A & D & G) | (B & E & H) | (C & F & I)`. This required 18 intermediate types, while the standard method
16842
+ // of expanding (A | B | C) & (D | E | F) & (G | H | I) would produce 27 types and then perform reduction on the result.
16843
+ // By going elemnt-wise, and bailing if the result fails to reduce, we can allow these sparse expansions without doing undue work.
16844
+ runningResult = typeSet[0];
16845
+ for (let i = 1; i < typeSet.length; i++) {
16846
+ // For intersection reduction, here we're considering `undefined & (A | B)` as `never`. (ie, we're disallowing branded primitives)
16847
+ // This is relevant for, eg, when looking at `(HTMLElement | null) & (SVGElement | null) & ... & undefined` where _usually_
16848
+ // we'd allow for tons of garbage intermediate types like `null & SVGElement` to exist; but nobody ever really actually _wants_
16849
+ // that, IMO. Those types can still exist in the type system; just... not when working with unions and intersections with massive
16850
+ // cross-product growth potential.
16851
+ runningResult = typeSet[i].flags & TypeFlags.Primitive && everyType(runningResult, t => !!(t.flags & TypeFlags.Object)) ? neverType : getReducedType(intersectTypes(runningResult, typeSet[i]));
16852
+ if (i === typeSet.length - 1 || isTypeAny(runningResult) || runningResult.flags & TypeFlags.Never) {
16853
+ return runningResult;
16854
+ }
16855
+ if (!(runningResult.flags & TypeFlags.Union) || (runningResult as UnionType).types.length > typeSet.length) {
16856
+ // Save work done by the accumulated result thus far, even if we're bailing on the heuristic.
16857
+ // It may have saved us enough work already that we're willing to work with the type now.
16858
+ typeSet = typeSet.slice(i + 1);
16859
+ break;
16860
+ }
16861
+ }
16862
+ }
16794
16863
// We are attempting to construct a type of the form X & (A | B) & (C | D). Transform this into a type of
16795
16864
// the form X & A & C | X & A & D | X & B & C | X & B & D. If the estimated size of the resulting union type
16796
16865
// exceeds 100000 constituents, report an error.
16797
16866
if (!checkCrossProductUnion(typeSet)) {
16798
16867
return errorType;
16799
16868
}
16800
16869
const constituents = getCrossProductIntersections(typeSet);
16801
- // We attach a denormalized origin type when at least one constituent of the cross-product union is an
16802
- // intersection (i.e. when the intersection didn't just reduce one or more unions to smaller unions) and
16803
- // the denormalized origin has fewer constituents than the union itself.
16804
- const origin = some(constituents, t => !!(t.flags & TypeFlags.Intersection)) && getConstituentCountOfTypes(constituents) > getConstituentCountOfTypes(typeSet) ? createOriginUnionOrIntersectionType(TypeFlags.Intersection, typeSet) : undefined;
16805
- result = getUnionType(constituents, UnionReduction.Literal, aliasSymbol, aliasTypeArguments, origin);
16870
+ if (runningResult && runningResult !== typeSet[0]) {
16871
+ result = getIntersectionType([runningResult, getUnionType(constituents, UnionReduction.Literal)], aliasSymbol, aliasTypeArguments);
16872
+ }
16873
+ else {
16874
+ // We attach a denormalized origin type when at least one constituent of the cross-product union is an
16875
+ // intersection (i.e. when the intersection didn't just reduce one or more unions to smaller unions) and
16876
+ // the denormalized origin has fewer constituents than the union itself.
16877
+ const origin = some(constituents, t => !!(t.flags & TypeFlags.Intersection)) && getConstituentCountOfTypes(constituents) > getConstituentCountOfTypes(typeSet) ? createOriginUnionOrIntersectionType(TypeFlags.Intersection, originalSet) : undefined;
16878
+ result = getUnionType(constituents, UnionReduction.Literal, aliasSymbol, aliasTypeArguments, origin);
16879
+ }
16806
16880
}
16807
16881
}
16808
16882
else {
@@ -16819,7 +16893,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
16819
16893
16820
16894
function checkCrossProductUnion(types: readonly Type[]) {
16821
16895
const size = getCrossProductUnionSize(types);
16822
- if (size >= 100000 ) {
16896
+ if (size >= UNION_CROSS_PRODUCT_SIZE_LIMIT ) {
16823
16897
tracing?.instant(tracing.Phase.CheckTypes, "checkCrossProductUnion_DepthLimit", { typeIds: types.map(t => t.id), size });
16824
16898
error(currentNode, Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent);
16825
16899
return false;
0 commit comments