From 685271621f34287aede3cd85bced18c7ece7b5a6 Mon Sep 17 00:00:00 2001 From: Matt McCutchen Date: Thu, 19 Jul 2018 18:54:32 -0400 Subject: [PATCH] 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. --- src/compiler/checker.ts | 38 ++++- .../intersectionsOfLargeUnions2.errors.txt | 45 ++++++ .../reference/intersectionsOfLargeUnions2.js | 61 ++++++++ .../intersectionsOfLargeUnions2.symbols | 115 ++++++++++++++++ .../intersectionsOfLargeUnions2.types | 130 ++++++++++++++++++ .../compiler/intersectionsOfLargeUnions2.ts | 37 +++++ 6 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 tests/baselines/reference/intersectionsOfLargeUnions2.errors.txt create mode 100644 tests/baselines/reference/intersectionsOfLargeUnions2.js create mode 100644 tests/baselines/reference/intersectionsOfLargeUnions2.symbols create mode 100644 tests/baselines/reference/intersectionsOfLargeUnions2.types create mode 100644 tests/cases/compiler/intersectionsOfLargeUnions2.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index e01c97af49222..d6a0bfbcbea18 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8874,10 +8874,42 @@ namespace ts { } // We are attempting to construct a type of the form X & (A | B) & Y. Transform this into a type of // the form X & A & Y | X & B & Y and recursively reduce until no union type constituents remain. - const unionIndex = findIndex(typeSet, t => (t.flags & TypeFlags.Union) !== 0); + const lastNonfinalUnionIndex = findLastIndex(typeSet, t => (t.flags & TypeFlags.Union) !== 0, typeSet.length - 2); + let partialIntersectionStartIndex: number, unionIndex: number; + if (lastNonfinalUnionIndex === -1) { + // typeSet[typeSet.length - 1] must be the only union. Distribute it and we're done. + partialIntersectionStartIndex = 0; + unionIndex = typeSet.length - 1; + } + else { + // `keyof` a large union of types results in an intersection of unions containing many unit types (GH#24223). + // To help avoid an exponential blowup, distribute the last union over the later constituents of the + // intersection and simplify the resulting union before distributing earlier unions. (Exception: don't + // distribute a union that is the last constituent of the intersection over the zero remaining constituents + // because that would have no effect.) + partialIntersectionStartIndex = lastNonfinalUnionIndex; + unionIndex = lastNonfinalUnionIndex; + } const unionType = typeSet[unionIndex]; - return getUnionType(map(unionType.types, t => getIntersectionType(replaceElement(typeSet, unionIndex, t))), - UnionReduction.Literal, aliasSymbol, aliasTypeArguments); + let relevantUnionMembers = unionType.types; + // As of 2018-07-19, discarding mismatching unit types here rather than letting it + // happen when we create the distributed union gives a 5x speedup on the test case + // for #23977. + if (includes & TypeFlags.Unit) { + const unitTypeInIntersection = find(typeSet, t => (t.flags & TypeFlags.Unit) !== 0)!; + relevantUnionMembers = filter(unionType.types, t => t === unitTypeInIntersection || (t.flags & TypeFlags.Unit) === 0); + } + const partialIntersectionMembers = typeSet.slice(partialIntersectionStartIndex); + const distributedMembers = map(relevantUnionMembers, t => getIntersectionType(replaceElement(partialIntersectionMembers, unionIndex - partialIntersectionStartIndex, t))); + if (partialIntersectionStartIndex === 0) { + return getUnionType(distributedMembers, UnionReduction.Literal, aliasSymbol, aliasTypeArguments); + } + else { + const distributedUnion = getUnionType(distributedMembers, UnionReduction.Literal); + const newIntersectionMembers = typeSet.slice(0, partialIntersectionStartIndex + 1); + newIntersectionMembers[partialIntersectionStartIndex] = distributedUnion; + return getIntersectionType(newIntersectionMembers, aliasSymbol, aliasTypeArguments); + } } const id = getTypeListId(typeSet); let type = intersectionTypes.get(id); diff --git a/tests/baselines/reference/intersectionsOfLargeUnions2.errors.txt b/tests/baselines/reference/intersectionsOfLargeUnions2.errors.txt new file mode 100644 index 0000000000000..bbe9ee1d2f70a --- /dev/null +++ b/tests/baselines/reference/intersectionsOfLargeUnions2.errors.txt @@ -0,0 +1,45 @@ +tests/cases/compiler/intersectionsOfLargeUnions2.ts(31,15): error TS2536: Type 'T' cannot be used to index type 'HTMLElementTagNameMap'. +tests/cases/compiler/intersectionsOfLargeUnions2.ts(31,15): error TS2536: Type 'P' cannot be used to index type 'HTMLElementTagNameMap[T]'. + + +==== tests/cases/compiler/intersectionsOfLargeUnions2.ts (2 errors) ==== + // Repro from #24223 + + declare global { + interface ElementTagNameMap { + [index: number]: HTMLElement + } + + interface HTMLElement { + [index: number]: HTMLElement; + } + } + + export function assertIsElement(node: Node | null): node is Element { + let nodeType = node === null ? null : node.nodeType; + return nodeType === 1; + } + + export function assertNodeTagName< + T extends keyof ElementTagNameMap, + U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U { + if (assertIsElement(node)) { + const nodeTagName = node.tagName.toLowerCase(); + return nodeTagName === tagName; + } + return false; + } + + export function assertNodeProperty< + T extends keyof ElementTagNameMap, + P extends keyof ElementTagNameMap[T], + V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) { + ~~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2536: Type 'T' cannot be used to index type 'HTMLElementTagNameMap'. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2536: Type 'P' cannot be used to index type 'HTMLElementTagNameMap[T]'. + if (assertNodeTagName(node, tagName)) { + node[prop]; + } + } + \ No newline at end of file diff --git a/tests/baselines/reference/intersectionsOfLargeUnions2.js b/tests/baselines/reference/intersectionsOfLargeUnions2.js new file mode 100644 index 0000000000000..99f393bb6e9fd --- /dev/null +++ b/tests/baselines/reference/intersectionsOfLargeUnions2.js @@ -0,0 +1,61 @@ +//// [intersectionsOfLargeUnions2.ts] +// Repro from #24223 + +declare global { + interface ElementTagNameMap { + [index: number]: HTMLElement + } + + interface HTMLElement { + [index: number]: HTMLElement; + } +} + +export function assertIsElement(node: Node | null): node is Element { + let nodeType = node === null ? null : node.nodeType; + return nodeType === 1; +} + +export function assertNodeTagName< + T extends keyof ElementTagNameMap, + U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U { + if (assertIsElement(node)) { + const nodeTagName = node.tagName.toLowerCase(); + return nodeTagName === tagName; + } + return false; +} + +export function assertNodeProperty< + T extends keyof ElementTagNameMap, + P extends keyof ElementTagNameMap[T], + V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) { + if (assertNodeTagName(node, tagName)) { + node[prop]; + } +} + + +//// [intersectionsOfLargeUnions2.js] +"use strict"; +// Repro from #24223 +exports.__esModule = true; +function assertIsElement(node) { + var nodeType = node === null ? null : node.nodeType; + return nodeType === 1; +} +exports.assertIsElement = assertIsElement; +function assertNodeTagName(node, tagName) { + if (assertIsElement(node)) { + var nodeTagName = node.tagName.toLowerCase(); + return nodeTagName === tagName; + } + return false; +} +exports.assertNodeTagName = assertNodeTagName; +function assertNodeProperty(node, tagName, prop, value) { + if (assertNodeTagName(node, tagName)) { + node[prop]; + } +} +exports.assertNodeProperty = assertNodeProperty; diff --git a/tests/baselines/reference/intersectionsOfLargeUnions2.symbols b/tests/baselines/reference/intersectionsOfLargeUnions2.symbols new file mode 100644 index 0000000000000..e3ce64f74f4aa --- /dev/null +++ b/tests/baselines/reference/intersectionsOfLargeUnions2.symbols @@ -0,0 +1,115 @@ +=== tests/cases/compiler/intersectionsOfLargeUnions2.ts === +// Repro from #24223 + +declare global { +>global : Symbol(global, Decl(intersectionsOfLargeUnions2.ts, 0, 0)) + + interface ElementTagNameMap { +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --), Decl(intersectionsOfLargeUnions2.ts, 2, 16)) + + [index: number]: HTMLElement +>index : Symbol(index, Decl(intersectionsOfLargeUnions2.ts, 4, 9)) +>HTMLElement : Symbol(HTMLElement, Decl(intersectionsOfLargeUnions2.ts, 5, 5)) + } + + interface HTMLElement { +>HTMLElement : Symbol(HTMLElement, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --), Decl(intersectionsOfLargeUnions2.ts, 5, 5)) + + [index: number]: HTMLElement; +>index : Symbol(index, Decl(intersectionsOfLargeUnions2.ts, 8, 9)) +>HTMLElement : Symbol(HTMLElement, Decl(intersectionsOfLargeUnions2.ts, 5, 5)) + } +} + +export function assertIsElement(node: Node | null): node is Element { +>assertIsElement : Symbol(assertIsElement, Decl(intersectionsOfLargeUnions2.ts, 10, 1)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 12, 32)) +>Node : Symbol(Node, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 12, 32)) +>Element : Symbol(Element, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --)) + + let nodeType = node === null ? null : node.nodeType; +>nodeType : Symbol(nodeType, Decl(intersectionsOfLargeUnions2.ts, 13, 7)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 12, 32)) +>node.nodeType : Symbol(Node.nodeType, Decl(lib.dom.d.ts, --, --)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 12, 32)) +>nodeType : Symbol(Node.nodeType, Decl(lib.dom.d.ts, --, --)) + + return nodeType === 1; +>nodeType : Symbol(nodeType, Decl(intersectionsOfLargeUnions2.ts, 13, 7)) +} + +export function assertNodeTagName< +>assertNodeTagName : Symbol(assertNodeTagName, Decl(intersectionsOfLargeUnions2.ts, 15, 1)) + + T extends keyof ElementTagNameMap, +>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 17, 34)) +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --), Decl(intersectionsOfLargeUnions2.ts, 2, 16)) + + U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U { +>U : Symbol(U, Decl(intersectionsOfLargeUnions2.ts, 18, 38)) +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --), Decl(intersectionsOfLargeUnions2.ts, 2, 16)) +>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 17, 34)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 19, 36)) +>Node : Symbol(Node, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --)) +>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions2.ts, 19, 54)) +>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 17, 34)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 19, 36)) +>U : Symbol(U, Decl(intersectionsOfLargeUnions2.ts, 18, 38)) + + if (assertIsElement(node)) { +>assertIsElement : Symbol(assertIsElement, Decl(intersectionsOfLargeUnions2.ts, 10, 1)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 19, 36)) + + const nodeTagName = node.tagName.toLowerCase(); +>nodeTagName : Symbol(nodeTagName, Decl(intersectionsOfLargeUnions2.ts, 21, 13)) +>node.tagName.toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --)) +>node.tagName : Symbol(Element.tagName, Decl(lib.dom.d.ts, --, --)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 19, 36)) +>tagName : Symbol(Element.tagName, Decl(lib.dom.d.ts, --, --)) +>toLowerCase : Symbol(String.toLowerCase, Decl(lib.es5.d.ts, --, --)) + + return nodeTagName === tagName; +>nodeTagName : Symbol(nodeTagName, Decl(intersectionsOfLargeUnions2.ts, 21, 13)) +>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions2.ts, 19, 54)) + } + return false; +} + +export function assertNodeProperty< +>assertNodeProperty : Symbol(assertNodeProperty, Decl(intersectionsOfLargeUnions2.ts, 25, 1)) + + T extends keyof ElementTagNameMap, +>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 27, 35)) +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --), Decl(intersectionsOfLargeUnions2.ts, 2, 16)) + + P extends keyof ElementTagNameMap[T], +>P : Symbol(P, Decl(intersectionsOfLargeUnions2.ts, 28, 38)) +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.dom.d.ts, --, --), Decl(intersectionsOfLargeUnions2.ts, 2, 16)) +>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 27, 35)) + + V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) { +>V : Symbol(V, Decl(intersectionsOfLargeUnions2.ts, 29, 41)) +>HTMLElementTagNameMap : Symbol(HTMLElementTagNameMap, Decl(lib.dom.d.ts, --, --)) +>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 27, 35)) +>P : Symbol(P, Decl(intersectionsOfLargeUnions2.ts, 28, 38)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 30, 43)) +>Node : Symbol(Node, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --)) +>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions2.ts, 30, 61)) +>T : Symbol(T, Decl(intersectionsOfLargeUnions2.ts, 27, 35)) +>prop : Symbol(prop, Decl(intersectionsOfLargeUnions2.ts, 30, 73)) +>P : Symbol(P, Decl(intersectionsOfLargeUnions2.ts, 28, 38)) +>value : Symbol(value, Decl(intersectionsOfLargeUnions2.ts, 30, 82)) +>V : Symbol(V, Decl(intersectionsOfLargeUnions2.ts, 29, 41)) + + if (assertNodeTagName(node, tagName)) { +>assertNodeTagName : Symbol(assertNodeTagName, Decl(intersectionsOfLargeUnions2.ts, 15, 1)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 30, 43)) +>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions2.ts, 30, 61)) + + node[prop]; +>node : Symbol(node, Decl(intersectionsOfLargeUnions2.ts, 30, 43)) +>prop : Symbol(prop, Decl(intersectionsOfLargeUnions2.ts, 30, 73)) + } +} + diff --git a/tests/baselines/reference/intersectionsOfLargeUnions2.types b/tests/baselines/reference/intersectionsOfLargeUnions2.types new file mode 100644 index 0000000000000..2e203d0e2ece4 --- /dev/null +++ b/tests/baselines/reference/intersectionsOfLargeUnions2.types @@ -0,0 +1,130 @@ +=== tests/cases/compiler/intersectionsOfLargeUnions2.ts === +// Repro from #24223 + +declare global { +>global : any + + interface ElementTagNameMap { +>ElementTagNameMap : ElementTagNameMap + + [index: number]: HTMLElement +>index : number +>HTMLElement : global.HTMLElement + } + + interface HTMLElement { +>HTMLElement : HTMLElement + + [index: number]: HTMLElement; +>index : number +>HTMLElement : global.HTMLElement + } +} + +export function assertIsElement(node: Node | null): node is Element { +>assertIsElement : (node: Node | null) => node is Element +>node : Node | null +>Node : Node +>null : null +>node : any +>Element : Element + + let nodeType = node === null ? null : node.nodeType; +>nodeType : number | null +>node === null ? null : node.nodeType : number | null +>node === null : boolean +>node : Node | null +>null : null +>null : null +>node.nodeType : number +>node : Node +>nodeType : number + + return nodeType === 1; +>nodeType === 1 : boolean +>nodeType : number | null +>1 : 1 +} + +export function assertNodeTagName< +>assertNodeTagName : (node: Node | null, tagName: T) => node is U + + T extends keyof ElementTagNameMap, +>T : T +>ElementTagNameMap : ElementTagNameMap + + U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U { +>U : U +>ElementTagNameMap : ElementTagNameMap +>T : T +>node : Node | null +>Node : Node +>null : null +>tagName : T +>T : T +>node : any +>U : U + + if (assertIsElement(node)) { +>assertIsElement(node) : boolean +>assertIsElement : (node: Node | null) => node is Element +>node : Node | null + + const nodeTagName = node.tagName.toLowerCase(); +>nodeTagName : string +>node.tagName.toLowerCase() : string +>node.tagName.toLowerCase : () => string +>node.tagName : string +>node : Element +>tagName : string +>toLowerCase : () => string + + return nodeTagName === tagName; +>nodeTagName === tagName : boolean +>nodeTagName : string +>tagName : T + } + return false; +>false : false +} + +export function assertNodeProperty< +>assertNodeProperty : (node: Node | null, tagName: T, prop: P, value: V) => void + + T extends keyof ElementTagNameMap, +>T : T +>ElementTagNameMap : ElementTagNameMap + + P extends keyof ElementTagNameMap[T], +>P : P +>ElementTagNameMap : ElementTagNameMap +>T : T + + V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) { +>V : V +>HTMLElementTagNameMap : HTMLElementTagNameMap +>T : T +>P : P +>node : Node | null +>Node : Node +>null : null +>tagName : T +>T : T +>prop : P +>P : P +>value : V +>V : V + + if (assertNodeTagName(node, tagName)) { +>assertNodeTagName(node, tagName) : boolean +>assertNodeTagName : (node: Node | null, tagName: T) => node is U +>node : Node | null +>tagName : T + + node[prop]; +>node[prop] : ElementTagNameMap[T][P] +>node : ElementTagNameMap[T] +>prop : P + } +} + diff --git a/tests/cases/compiler/intersectionsOfLargeUnions2.ts b/tests/cases/compiler/intersectionsOfLargeUnions2.ts new file mode 100644 index 0000000000000..c459d8e77890d --- /dev/null +++ b/tests/cases/compiler/intersectionsOfLargeUnions2.ts @@ -0,0 +1,37 @@ +// @strict: true + +// Repro from #24223 + +declare global { + interface ElementTagNameMap { + [index: number]: HTMLElement + } + + interface HTMLElement { + [index: number]: HTMLElement; + } +} + +export function assertIsElement(node: Node | null): node is Element { + let nodeType = node === null ? null : node.nodeType; + return nodeType === 1; +} + +export function assertNodeTagName< + T extends keyof ElementTagNameMap, + U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U { + if (assertIsElement(node)) { + const nodeTagName = node.tagName.toLowerCase(); + return nodeTagName === tagName; + } + return false; +} + +export function assertNodeProperty< + T extends keyof ElementTagNameMap, + P extends keyof ElementTagNameMap[T], + V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) { + if (assertNodeTagName(node, tagName)) { + node[prop]; + } +}