Skip to content

Commit 6852716

Browse files
committed
Smarter algorithm to distribute intersections of unions.
Helps avoid exponential blowup for `keyof` large unions even when `keyof` each type in the union is not a union of unit types (e.g., because there is an index signature or a type variable). Fixes #24223.
1 parent 1cedab1 commit 6852716

6 files changed

+423
-3
lines changed

src/compiler/checker.ts

+35-3
Original file line numberDiff line numberDiff line change
@@ -8874,10 +8874,42 @@ namespace ts {
88748874
}
88758875
// We are attempting to construct a type of the form X & (A | B) & Y. Transform this into a type of
88768876
// the form X & A & Y | X & B & Y and recursively reduce until no union type constituents remain.
8877-
const unionIndex = findIndex(typeSet, t => (t.flags & TypeFlags.Union) !== 0);
8877+
const lastNonfinalUnionIndex = findLastIndex(typeSet, t => (t.flags & TypeFlags.Union) !== 0, typeSet.length - 2);
8878+
let partialIntersectionStartIndex: number, unionIndex: number;
8879+
if (lastNonfinalUnionIndex === -1) {
8880+
// typeSet[typeSet.length - 1] must be the only union. Distribute it and we're done.
8881+
partialIntersectionStartIndex = 0;
8882+
unionIndex = typeSet.length - 1;
8883+
}
8884+
else {
8885+
// `keyof` a large union of types results in an intersection of unions containing many unit types (GH#24223).
8886+
// To help avoid an exponential blowup, distribute the last union over the later constituents of the
8887+
// intersection and simplify the resulting union before distributing earlier unions. (Exception: don't
8888+
// distribute a union that is the last constituent of the intersection over the zero remaining constituents
8889+
// because that would have no effect.)
8890+
partialIntersectionStartIndex = lastNonfinalUnionIndex;
8891+
unionIndex = lastNonfinalUnionIndex;
8892+
}
88788893
const unionType = <UnionType>typeSet[unionIndex];
8879-
return getUnionType(map(unionType.types, t => getIntersectionType(replaceElement(typeSet, unionIndex, t))),
8880-
UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
8894+
let relevantUnionMembers = unionType.types;
8895+
// As of 2018-07-19, discarding mismatching unit types here rather than letting it
8896+
// happen when we create the distributed union gives a 5x speedup on the test case
8897+
// for #23977.
8898+
if (includes & TypeFlags.Unit) {
8899+
const unitTypeInIntersection = find(typeSet, t => (t.flags & TypeFlags.Unit) !== 0)!;
8900+
relevantUnionMembers = filter(unionType.types, t => t === unitTypeInIntersection || (t.flags & TypeFlags.Unit) === 0);
8901+
}
8902+
const partialIntersectionMembers = typeSet.slice(partialIntersectionStartIndex);
8903+
const distributedMembers = map(relevantUnionMembers, t => getIntersectionType(replaceElement(partialIntersectionMembers, unionIndex - partialIntersectionStartIndex, t)));
8904+
if (partialIntersectionStartIndex === 0) {
8905+
return getUnionType(distributedMembers, UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
8906+
}
8907+
else {
8908+
const distributedUnion = getUnionType(distributedMembers, UnionReduction.Literal);
8909+
const newIntersectionMembers = typeSet.slice(0, partialIntersectionStartIndex + 1);
8910+
newIntersectionMembers[partialIntersectionStartIndex] = distributedUnion;
8911+
return getIntersectionType(newIntersectionMembers, aliasSymbol, aliasTypeArguments);
8912+
}
88818913
}
88828914
const id = getTypeListId(typeSet);
88838915
let type = intersectionTypes.get(id);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
tests/cases/compiler/intersectionsOfLargeUnions2.ts(31,15): error TS2536: Type 'T' cannot be used to index type 'HTMLElementTagNameMap'.
2+
tests/cases/compiler/intersectionsOfLargeUnions2.ts(31,15): error TS2536: Type 'P' cannot be used to index type 'HTMLElementTagNameMap[T]'.
3+
4+
5+
==== tests/cases/compiler/intersectionsOfLargeUnions2.ts (2 errors) ====
6+
// Repro from #24223
7+
8+
declare global {
9+
interface ElementTagNameMap {
10+
[index: number]: HTMLElement
11+
}
12+
13+
interface HTMLElement {
14+
[index: number]: HTMLElement;
15+
}
16+
}
17+
18+
export function assertIsElement(node: Node | null): node is Element {
19+
let nodeType = node === null ? null : node.nodeType;
20+
return nodeType === 1;
21+
}
22+
23+
export function assertNodeTagName<
24+
T extends keyof ElementTagNameMap,
25+
U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U {
26+
if (assertIsElement(node)) {
27+
const nodeTagName = node.tagName.toLowerCase();
28+
return nodeTagName === tagName;
29+
}
30+
return false;
31+
}
32+
33+
export function assertNodeProperty<
34+
T extends keyof ElementTagNameMap,
35+
P extends keyof ElementTagNameMap[T],
36+
V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) {
37+
~~~~~~~~~~~~~~~~~~~~~~~~
38+
!!! error TS2536: Type 'T' cannot be used to index type 'HTMLElementTagNameMap'.
39+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
40+
!!! error TS2536: Type 'P' cannot be used to index type 'HTMLElementTagNameMap[T]'.
41+
if (assertNodeTagName(node, tagName)) {
42+
node[prop];
43+
}
44+
}
45+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//// [intersectionsOfLargeUnions2.ts]
2+
// Repro from #24223
3+
4+
declare global {
5+
interface ElementTagNameMap {
6+
[index: number]: HTMLElement
7+
}
8+
9+
interface HTMLElement {
10+
[index: number]: HTMLElement;
11+
}
12+
}
13+
14+
export function assertIsElement(node: Node | null): node is Element {
15+
let nodeType = node === null ? null : node.nodeType;
16+
return nodeType === 1;
17+
}
18+
19+
export function assertNodeTagName<
20+
T extends keyof ElementTagNameMap,
21+
U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U {
22+
if (assertIsElement(node)) {
23+
const nodeTagName = node.tagName.toLowerCase();
24+
return nodeTagName === tagName;
25+
}
26+
return false;
27+
}
28+
29+
export function assertNodeProperty<
30+
T extends keyof ElementTagNameMap,
31+
P extends keyof ElementTagNameMap[T],
32+
V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) {
33+
if (assertNodeTagName(node, tagName)) {
34+
node[prop];
35+
}
36+
}
37+
38+
39+
//// [intersectionsOfLargeUnions2.js]
40+
"use strict";
41+
// Repro from #24223
42+
exports.__esModule = true;
43+
function assertIsElement(node) {
44+
var nodeType = node === null ? null : node.nodeType;
45+
return nodeType === 1;
46+
}
47+
exports.assertIsElement = assertIsElement;
48+
function assertNodeTagName(node, tagName) {
49+
if (assertIsElement(node)) {
50+
var nodeTagName = node.tagName.toLowerCase();
51+
return nodeTagName === tagName;
52+
}
53+
return false;
54+
}
55+
exports.assertNodeTagName = assertNodeTagName;
56+
function assertNodeProperty(node, tagName, prop, value) {
57+
if (assertNodeTagName(node, tagName)) {
58+
node[prop];
59+
}
60+
}
61+
exports.assertNodeProperty = assertNodeProperty;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
=== tests/cases/compiler/intersectionsOfLargeUnions2.ts ===
2+
// Repro from #24223
3+
4+
declare global {
5+
>global : Symbol(global, Decl(intersectionsOfLargeUnions2.ts, 0, 0))
6+
7+
interface ElementTagNameMap {
8+
>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --), Decl(intersectionsOfLargeUnions2.ts, 2, 16))
9+
10+
[index: number]: HTMLElement
11+
>index : Symbol(index, Decl(intersectionsOfLargeUnions2.ts, 4, 9))
12+
>HTMLElement : Symbol(HTMLElement, Decl(intersectionsOfLargeUnions2.ts, 5, 5))
13+
}
14+
15+
interface HTMLElement {
16+
>HTMLElement : Symbol(HTMLElement, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --), Decl(intersectionsOfLargeUnions2.ts, 5, 5))
17+
18+
[index: number]: HTMLElement;
19+
>index : Symbol(index, Decl(intersectionsOfLargeUnions2.ts, 8, 9))
20+
>HTMLElement : Symbol(HTMLElement, Decl(intersectionsOfLargeUnions2.ts, 5, 5))
21+
}
22+
}
23+
24+
export function assertIsElement(node: Node | null): node is Element {
25+
>assertIsElement : Symbol(assertIsElement, Decl(intersectionsOfLargeUnions2.ts, 10, 1))
26+
>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 12, 32))
27+
>Node : Symbol(Node, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --))
28+
>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 12, 32))
29+
>Element : Symbol(Element, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --))
30+
31+
let nodeType = node === null ? null : node.nodeType;
32+
>nodeType : Symbol(nodeType, Decl(intersectionsOfLargeUnions2.ts, 13, 7))
33+
>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 12, 32))
34+
>node.nodeType : Symbol(Node.nodeType, Decl(lib.dom.d.ts, --, --))
35+
>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 12, 32))
36+
>nodeType : Symbol(Node.nodeType, Decl(lib.dom.d.ts, --, --))
37+
38+
return nodeType === 1;
39+
>nodeType : Symbol(nodeType, Decl(intersectionsOfLargeUnions2.ts, 13, 7))
40+
}
41+
42+
export function assertNodeTagName<
43+
>assertNodeTagName : Symbol(assertNodeTagName, Decl(intersectionsOfLargeUnions2.ts, 15, 1))
44+
45+
T extends keyof ElementTagNameMap,
46+
>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 17, 34))
47+
>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --), Decl(intersectionsOfLargeUnions2.ts, 2, 16))
48+
49+
U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U {
50+
>U : Symbol(U, Decl(intersectionsOfLargeUnions2.ts, 18, 38))
51+
>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --), Decl(intersectionsOfLargeUnions2.ts, 2, 16))
52+
>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 17, 34))
53+
>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 19, 36))
54+
>Node : Symbol(Node, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --))
55+
>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions2.ts, 19, 54))
56+
>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 17, 34))
57+
>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 19, 36))
58+
>U : Symbol(U, Decl(intersectionsOfLargeUnions2.ts, 18, 38))
59+
60+
if (assertIsElement(node)) {
61+
>assertIsElement : Symbol(assertIsElement, Decl(intersectionsOfLargeUnions2.ts, 10, 1))
62+
>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 19, 36))
63+
64+
const nodeTagName = node.tagName.toLowerCase();
65+
>nodeTagName : Symbol(nodeTagName, Decl(intersectionsOfLargeUnions2.ts, 21, 13))
66+
>node.tagName.toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
67+
>node.tagName : Symbol(Element.tagName, Decl(lib.dom.d.ts, --, --))
68+
>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 19, 36))
69+
>tagName : Symbol(Element.tagName, Decl(lib.dom.d.ts, --, --))
70+
>toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --))
71+
72+
return nodeTagName === tagName;
73+
>nodeTagName : Symbol(nodeTagName, Decl(intersectionsOfLargeUnions2.ts, 21, 13))
74+
>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions2.ts, 19, 54))
75+
}
76+
return false;
77+
}
78+
79+
export function assertNodeProperty<
80+
>assertNodeProperty : Symbol(assertNodeProperty, Decl(intersectionsOfLargeUnions2.ts, 25, 1))
81+
82+
T extends keyof ElementTagNameMap,
83+
>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 27, 35))
84+
>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --), Decl(intersectionsOfLargeUnions2.ts, 2, 16))
85+
86+
P extends keyof ElementTagNameMap[T],
87+
>P : Symbol(P, Decl(intersectionsOfLargeUnions2.ts, 28, 38))
88+
>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --), Decl(intersectionsOfLargeUnions2.ts, 2, 16))
89+
>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 27, 35))
90+
91+
V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) {
92+
>V : Symbol(V, Decl(intersectionsOfLargeUnions2.ts, 29, 41))
93+
>HTMLElementTagNameMap : Symbol(HTMLElementTagNameMap, Decl(lib.dom.d.ts, --, --))
94+
>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 27, 35))
95+
>P : Symbol(P, Decl(intersectionsOfLargeUnions2.ts, 28, 38))
96+
>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 30, 43))
97+
>Node : Symbol(Node, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --))
98+
>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions2.ts, 30, 61))
99+
>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 27, 35))
100+
>prop : Symbol(prop, Decl(intersectionsOfLargeUnions2.ts, 30, 73))
101+
>P : Symbol(P, Decl(intersectionsOfLargeUnions2.ts, 28, 38))
102+
>value : Symbol(value, Decl(intersectionsOfLargeUnions2.ts, 30, 82))
103+
>V : Symbol(V, Decl(intersectionsOfLargeUnions2.ts, 29, 41))
104+
105+
if (assertNodeTagName(node, tagName)) {
106+
>assertNodeTagName : Symbol(assertNodeTagName, Decl(intersectionsOfLargeUnions2.ts, 15, 1))
107+
>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 30, 43))
108+
>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions2.ts, 30, 61))
109+
110+
node[prop];
111+
>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 30, 43))
112+
>prop : Symbol(prop, Decl(intersectionsOfLargeUnions2.ts, 30, 73))
113+
}
114+
}
115+

0 commit comments

Comments
 (0)