From bcc6aa8f386726f66a39425af600d9bcf5d19f58 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 9 Dec 2020 11:51:34 -0800 Subject: [PATCH 1/2] Generalize the fastpath for comparisons of unions which are correspondences to unions resulting from the application of intersections --- src/compiler/checker.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 9f85b552378ca..a894ae9649cbb 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -17258,14 +17258,29 @@ namespace ts { return Ternary.False; } + function getUndefinedStrippedTargetIfNeeded(source: Type, target: Type) { + // As a builtin type, `undefined` is a very low type ID - making it almsot always first, making this a very fast check to see + // if we need to strip `undefined` from the target + if (source.flags & TypeFlags.Union && target.flags & TypeFlags.Union && + !((source as UnionType).types[0].flags & TypeFlags.Undefined) && (target as UnionType).types[0].flags & TypeFlags.Undefined) { + return extractTypesOfKind(target, ~TypeFlags.Undefined); + } + return target; + } + function eachTypeRelatedToType(source: UnionOrIntersectionType, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { let result = Ternary.True; const sourceTypes = source.types; + const undefinedStrippedTarget = getUndefinedStrippedTargetIfNeeded(source, target as UnionType); for (let i = 0; i < sourceTypes.length; i++) { const sourceType = sourceTypes[i]; - if (target.flags & TypeFlags.Union && (target as UnionType).types.length === sourceTypes.length) { + if (undefinedStrippedTarget.flags & TypeFlags.Union && sourceTypes.length >= (undefinedStrippedTarget as UnionType).types.length && sourceTypes.length % (undefinedStrippedTarget as UnionType).types.length === 0) { // many unions are mappings of one another; in such cases, simply comparing members at the same index can shortcut the comparison - const related = isRelatedTo(sourceType, (target as UnionType).types[i], /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState); + // such unions will have identical lengths, and their corresponding elements will match up. Another common scenario is where a large + // union has a union of objects intersected with it. In such cases, if the input was, eg `("a" | "b" | "c") & (string | boolean | {} | {whatever})`, + // the result will have the structure `"a" | "b" | "c" | "a" & {} | "b" & {} | "c" & {} | "a" & {whatever} | "b" & {whatever} | "c" & {whatever}` + // - the resulting union has a length which is a multiple of the original union, and the elements correspond modulo the length of the original union + const related = isRelatedTo(sourceType, (undefinedStrippedTarget as UnionType).types[i % (undefinedStrippedTarget as UnionType).types.length], /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState); if (related) { result &= related; continue; From 8ee7b38193830c916418e2fe6efecf678692922d Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 15 Dec 2020 13:21:10 -0800 Subject: [PATCH 2/2] Add comment --- src/compiler/checker.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a894ae9649cbb..5b5aca7b8c013 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -17271,6 +17271,8 @@ namespace ts { function eachTypeRelatedToType(source: UnionOrIntersectionType, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { let result = Ternary.True; const sourceTypes = source.types; + // We strip `undefined` from the target if the `source` trivially doesn't contain it for our correspondence-checking fastpath + // since `undefined` is frequently added by optionality and would otherwise spoil a potentially useful correspondence const undefinedStrippedTarget = getUndefinedStrippedTargetIfNeeded(source, target as UnionType); for (let i = 0; i < sourceTypes.length; i++) { const sourceType = sourceTypes[i];