diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 01c8ef4f34..26a2e9fbc7 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8529,6 +8529,15 @@ namespace ts { return binarySearch(types, type, getTypeId, compareValues) >= 0; } + function insertType(types: Type[], type: Type): boolean { + const index = binarySearch(types, type, getTypeId, compareValues); + if (index < 0) { + types.splice(~index, 0, type); + return true; + } + return false; + } + // Return true if the given intersection type contains // more than one unit type or, // an object type and a nullable type (null or undefined), or @@ -8696,7 +8705,7 @@ namespace ts { includes & TypeFlags.Undefined ? includes & TypeFlags.NonWideningType ? undefinedType : undefinedWideningType : neverType; } - return getUnionTypeFromSortedList(typeSet, includes & TypeFlags.NotUnit ? 0 : TypeFlags.UnionOfUnitTypes, aliasSymbol, aliasTypeArguments); + return getUnionTypeFromSortedList(typeSet, includes & TypeFlags.NotPrimitiveUnion ? 0 : TypeFlags.UnionOfPrimitiveTypes, aliasSymbol, aliasTypeArguments); } function getUnionTypePredicate(signatures: ReadonlyArray): TypePredicate | undefined { @@ -8819,26 +8828,63 @@ namespace ts { } } - // When intersecting unions of unit types we can simply intersect based on type identity. - // Here we remove all unions of unit types from the given list and replace them with a - // a single union containing an intersection of the unit types. - function intersectUnionsOfUnitTypes(types: Type[]) { - const unionIndex = findIndex(types, t => (t.flags & TypeFlags.UnionOfUnitTypes) !== 0); - const unionType = types[unionIndex]; - let intersection = unionType.types; - let i = types.length - 1; - while (i > unionIndex) { + // Check that the given type has a match in every union. A given type is matched by + // an identical type, and a literal type is additionally matched by its corresponding + // primitive type. + function eachUnionContains(unionTypes: UnionType[], type: Type) { + for (const u of unionTypes) { + if (!containsType(u.types, type)) { + const primitive = type.flags & TypeFlags.StringLiteral ? stringType : + type.flags & TypeFlags.NumberLiteral ? numberType : + type.flags & TypeFlags.UniqueESSymbol ? esSymbolType : + undefined; + if (!primitive || !containsType(u.types, primitive)) { + return false; + } + } + } + return true; + } + + // If the given list of types contains more than one union of primitive types, replace the + // first with a union containing an intersection of those primitive types, then remove the + // other unions and return true. Otherwise, do nothing and return false. + function intersectUnionsOfPrimitiveTypes(types: Type[]) { + let unionTypes: UnionType[] | undefined; + const index = findIndex(types, t => (t.flags & TypeFlags.UnionOfPrimitiveTypes) !== 0); + let i = index + 1; + // Remove all but the first union of primitive types and collect them in + // the unionTypes array. + while (i < types.length) { const t = types[i]; - if (t.flags & TypeFlags.UnionOfUnitTypes) { - intersection = filter(intersection, u => containsType((t).types, u)); + if (t.flags & TypeFlags.UnionOfPrimitiveTypes) { + (unionTypes || (unionTypes = [types[index]])).push(t); orderedRemoveItemAt(types, i); } - i--; + else { + i++; + } } - if (intersection === unionType.types) { + // Return false if there was only one union of primitive types + if (!unionTypes) { return false; } - types[unionIndex] = getUnionTypeFromSortedList(intersection, unionType.flags & TypeFlags.UnionOfUnitTypes); + // We have more than one union of primitive types, now intersect them. For each + // type in each union we check if the type is matched in every union and if so + // we include it in the result. + const checked: Type[] = []; + const result: Type[] = []; + for (const u of unionTypes) { + for (const t of u.types) { + if (insertType(checked, t)) { + if (eachUnionContains(unionTypes, t)) { + insertType(result, t); + } + } + } + } + // Finally replace the first union with the result + types[index] = getUnionTypeFromSortedList(result, TypeFlags.UnionOfPrimitiveTypes); return true; } @@ -8879,7 +8925,7 @@ namespace ts { return typeSet[0]; } if (includes & TypeFlags.Union) { - if (includes & TypeFlags.UnionOfUnitTypes && intersectUnionsOfUnitTypes(typeSet)) { + if (includes & TypeFlags.UnionOfPrimitiveTypes && intersectUnionsOfPrimitiveTypes(typeSet)) { // When the intersection creates a reduced set (which might mean that *all* union types have // disappeared), we restart the operation to get a new set of combined flags. Once we have // reduced we'll never reduce again, so this occurs at most once. @@ -13976,7 +14022,7 @@ namespace ts { if (type.flags & TypeFlags.Union) { const types = (type).types; const filtered = filter(types, f); - return filtered === types ? type : getUnionTypeFromSortedList(filtered, type.flags & TypeFlags.UnionOfUnitTypes); + return filtered === types ? type : getUnionTypeFromSortedList(filtered, type.flags & TypeFlags.UnionOfPrimitiveTypes); } return f(type) ? type : neverType; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 87664652b3..e1f5dcfa4e 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3675,7 +3675,7 @@ namespace ts { /* @internal */ FreshLiteral = 1 << 25, // Fresh literal or unique type /* @internal */ - UnionOfUnitTypes = 1 << 26, // Type is union of unit types + UnionOfPrimitiveTypes = 1 << 26, // Type is union of primitive types /* @internal */ ContainsWideningType = 1 << 27, // Type is or contains undefined or null widening type /* @internal */ @@ -3720,7 +3720,7 @@ namespace ts { Narrowable = Any | Unknown | StructuredOrInstantiable | StringLike | NumberLike | BooleanLike | ESSymbol | UniqueESSymbol | NonPrimitive, NotUnionOrUnit = Any | Unknown | ESSymbol | Object | NonPrimitive, /* @internal */ - NotUnit = Any | String | Number | Boolean | Enum | ESSymbol | Void | Never | StructuredOrInstantiable, + NotPrimitiveUnion = Any | Unknown | Enum | Void | Never | StructuredOrInstantiable, /* @internal */ RequiresWidening = ContainsWideningType | ContainsObjectLiteral, /* @internal */ diff --git a/tests/baselines/reference/intersectionsOfLargeUnions2.errors.txt b/tests/baselines/reference/intersectionsOfLargeUnions2.errors.txt new file mode 100644 index 0000000000..664569582b --- /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 #24233 + + 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 0000000000..80add6cd5c --- /dev/null +++ b/tests/baselines/reference/intersectionsOfLargeUnions2.js @@ -0,0 +1,61 @@ +//// [intersectionsOfLargeUnions2.ts] +// Repro from #24233 + +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 #24233 +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 0000000000..fc2ea00402 --- /dev/null +++ b/tests/baselines/reference/intersectionsOfLargeUnions2.symbols @@ -0,0 +1,115 @@ +=== tests/cases/compiler/intersectionsOfLargeUnions2.ts === +// Repro from #24233 + +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 0000000000..92edecd325 --- /dev/null +++ b/tests/baselines/reference/intersectionsOfLargeUnions2.types @@ -0,0 +1,130 @@ +=== tests/cases/compiler/intersectionsOfLargeUnions2.ts === +// Repro from #24233 + +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 0000000000..498e238a5b --- /dev/null +++ b/tests/cases/compiler/intersectionsOfLargeUnions2.ts @@ -0,0 +1,37 @@ +// @strict: true + +// Repro from #24233 + +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]; + } +}