Skip to content

Commit 538b5c8

Browse files
committed
Caching and quick discriminant checks in subtype reduction
1 parent 781c1d7 commit 538b5c8

File tree

1 file changed

+45
-12
lines changed

1 file changed

+45
-12
lines changed

src/compiler/checker.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,7 @@ namespace ts {
726726
const templateLiteralTypes = new Map<string, TemplateLiteralType>();
727727
const stringMappingTypes = new Map<string, StringMappingType>();
728728
const substitutionTypes = new Map<string, SubstitutionType>();
729+
const subtypeReductionCache = new Map<string, Type[]>();
729730
const evolvingArrayTypes: EvolvingArrayType[] = [];
730731
const undefinedProperties: SymbolTable = new Map();
731732

@@ -13320,19 +13321,51 @@ namespace ts {
1332013321
return includes;
1332113322
}
1332213323

13323-
function removeSubtypes(types: Type[], hasObjectTypes: boolean): boolean {
13324+
function removeSubtypes(types: Type[], hasObjectTypes: boolean): Type[] | undefined {
13325+
const id = getTypeListId(types);
13326+
const match = subtypeReductionCache.get(id);
13327+
if (match) {
13328+
return match;
13329+
}
1332413330
// We assume that redundant primitive types have already been removed from the types array and that there
1332513331
// are no any and unknown types in the array. Thus, the only possible supertypes for primitive types are empty
1332613332
// object types, and if none of those are present we can exclude primitive types from the subtype check.
1332713333
const hasEmptyObject = hasObjectTypes && some(types, t => !!(t.flags & TypeFlags.Object) && !isGenericMappedType(t) && isEmptyResolvedType(resolveStructuredTypeMembers(<ObjectType>t)));
1332813334
const len = types.length;
1332913335
let i = len;
13336+
let count = 0;
1333013337
while (i > 0) {
1333113338
i--;
1333213339
const source = types[i];
1333313340
if (hasEmptyObject || source.flags & TypeFlags.StructuredOrInstantiable) {
13341+
// Find the first property with a unit type, if any. When constituents have a property by the same name
13342+
// but of a different unit type, we can quickly disqualify them from subtype checks. This helps subtype
13343+
// reduction of large discriminated union types.
13344+
const keyProperty = source.flags & (TypeFlags.Object | TypeFlags.Intersection | TypeFlags.InstantiableNonPrimitive) ?
13345+
find(getPropertiesOfType(source), p => isUnitType(getTypeOfSymbol(p))) :
13346+
undefined;
13347+
const keyPropertyType = keyProperty && getRegularTypeOfLiteralType(getTypeOfSymbol(keyProperty));
1333413348
for (const target of types) {
1333513349
if (source !== target) {
13350+
if (count === 100000) {
13351+
// After 100000 subtype checks we estimate the remaining amount of work by assuming the
13352+
// same ratio of checks per element. If the estimated number of remaining type checks is
13353+
// greater than 1M we deem the union type too complex to represent. This for example
13354+
// caps union types at 1000 unique object types.
13355+
const estimatedCount = (count / (len - i)) * len;
13356+
if (estimatedCount > 1000000) {
13357+
tracing?.instant(tracing.Phase.CheckTypes, "removeSubtypes_DepthLimit", { typeIds: types.map(t => t.id) });
13358+
error(currentNode, Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent);
13359+
return undefined;
13360+
}
13361+
}
13362+
count++;
13363+
if (keyProperty && target.flags & (TypeFlags.Object | TypeFlags.Intersection | TypeFlags.InstantiableNonPrimitive)) {
13364+
const t = getTypeOfPropertyOfType(target, keyProperty.escapedName);
13365+
if (t && isUnitType(t) && getRegularTypeOfLiteralType(t) !== keyPropertyType) {
13366+
continue;
13367+
}
13368+
}
1333613369
if (isTypeRelatedTo(source, target, strictSubtypeRelation) && (
1333713370
!(getObjectFlags(getTargetType(source)) & ObjectFlags.Class) ||
1333813371
!(getObjectFlags(getTargetType(target)) & ObjectFlags.Class) ||
@@ -13344,7 +13377,8 @@ namespace ts {
1334413377
}
1334513378
}
1334613379
}
13347-
return true;
13380+
subtypeReductionCache.set(id, types);
13381+
return types;
1334813382
}
1334913383

1335013384
function removeRedundantLiteralTypes(types: Type[], includes: TypeFlags) {
@@ -13418,22 +13452,21 @@ namespace ts {
1341813452
if (types.length === 1) {
1341913453
return types[0];
1342013454
}
13421-
const typeSet: Type[] = [];
13455+
let typeSet: Type[] | undefined = [];
1342213456
const includes = addTypesToUnion(typeSet, 0, types);
1342313457
if (unionReduction !== UnionReduction.None) {
1342413458
if (includes & TypeFlags.AnyOrUnknown) {
1342513459
return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : unknownType;
1342613460
}
13427-
if (unionReduction & (UnionReduction.Literal | UnionReduction.Subtype)) {
13428-
if (includes & (TypeFlags.Literal | TypeFlags.UniqueESSymbol) || includes & TypeFlags.Void && includes & TypeFlags.Undefined) {
13429-
removeRedundantLiteralTypes(typeSet, includes);
13430-
}
13431-
if (includes & TypeFlags.StringLiteral && includes & TypeFlags.TemplateLiteral) {
13432-
removeStringLiteralsMatchedByTemplateLiterals(typeSet);
13433-
}
13461+
if (includes & (TypeFlags.Literal | TypeFlags.UniqueESSymbol) || includes & TypeFlags.Void && includes & TypeFlags.Undefined) {
13462+
removeRedundantLiteralTypes(typeSet, includes);
13463+
}
13464+
if (includes & TypeFlags.StringLiteral && includes & TypeFlags.TemplateLiteral) {
13465+
removeStringLiteralsMatchedByTemplateLiterals(typeSet);
1343413466
}
13435-
if (unionReduction & UnionReduction.Subtype && typeSet.length < 100) {
13436-
if (!removeSubtypes(typeSet, !!(includes & TypeFlags.Object))) {
13467+
if (unionReduction === UnionReduction.Subtype) {
13468+
typeSet = removeSubtypes(typeSet, !!(includes & TypeFlags.Object));
13469+
if (!typeSet) {
1343713470
return errorType;
1343813471
}
1343913472
}

0 commit comments

Comments
 (0)