-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Generalize the fastpath for comparisons of unions which are correspondences #41903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17258,14 +17258,31 @@ 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; | ||
// We strip `undefined` from the target if the `source` trivially doesn't contain it for our correspondence-checking fastpath | ||
weswigham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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++) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We seem to access There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ehh, it's a little awkward because we can only get it after the |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this order guaranteed somehow? If not, would we have someway of knowing that it changed, breaking this optimization? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kind-of. It's not guaranteed, just very often true. Often enough to be a useful heuristic. Unions are sorted by type IDs, type IDs are assigned on type construction, type are constructed in a top-down/left-to-right/as-they-appear kinda way when normalizing or mapping (mostly as an artifact of our mostly lazy compiler design). A notable thing that can easily break this ordering heuristic is code which pulls one of the type out of the union and makes it in advance, for example: type A = {a};
type B = {b};
type FooB = "foo" & B;
type Union = ("foo" | "bar") & (A | B); The presence of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That test that I commented on in the other PR would time out, for one. :V |
||
// 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; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
almost
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What types have lower type IDs? This seems potentially fragile.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only the
any
-likes are manufactured beforeundefined
(yes, this is arbitrary and fragile, but it turns out, also useful), and they can't exist in a union: soundefined
will actually always be the first element, if present.