From 426a63e8b6a98b2f6ff7eeddf98f0653582c744f Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Tue, 15 May 2018 12:24:40 -0700 Subject: [PATCH 1/4] Optimize intersections of unions of unit types --- src/compiler/checker.ts | 32 ++++++++++++++++++++++++++++---- src/compiler/types.ts | 4 ++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index caa75a45eb987..705ecea6b5ec1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8319,7 +8319,7 @@ namespace ts { includes & TypeFlags.Undefined ? includes & TypeFlags.NonWideningType ? undefinedType : undefinedWideningType : neverType; } - return getUnionTypeFromSortedList(typeSet, aliasSymbol, aliasTypeArguments); + return getUnionTypeFromSortedList(typeSet, includes & TypeFlags.NotUnit ? 0 : TypeFlags.UnionOfUnitTypes, aliasSymbol, aliasTypeArguments); } function getUnionTypePredicate(signatures: ReadonlyArray): TypePredicate { @@ -8359,7 +8359,7 @@ namespace ts { } // This function assumes the constituent type list is sorted and deduplicated. - function getUnionTypeFromSortedList(types: Type[], aliasSymbol?: Symbol, aliasTypeArguments?: Type[]): Type { + function getUnionTypeFromSortedList(types: Type[], unionOfUnitTypes: TypeFlags, aliasSymbol?: Symbol, aliasTypeArguments?: Type[]): Type { if (types.length === 0) { return neverType; } @@ -8370,7 +8370,7 @@ namespace ts { let type = unionTypes.get(id); if (!type) { const propagatedFlags = getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable); - type = createType(TypeFlags.Union | propagatedFlags); + type = createType(TypeFlags.Union | propagatedFlags | unionOfUnitTypes); unionTypes.set(id, type); type.types = types; /* @@ -8441,6 +8441,27 @@ 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) { + const t = types[i]; + if (t.flags & TypeFlags.UnionOfUnitTypes) { + intersection = filter(intersection, u => containsType((t).types, u)); + orderedRemoveItemAt(types, i); + } + i--; + } + if (intersection !== unionType.types) { + types[unionIndex] = getUnionTypeFromSortedList(intersection, unionType.flags & TypeFlags.UnionOfUnitTypes); + } + } + // We normalize combinations of intersection and union types based on the distributive property of the '&' // operator. Specifically, because X & (A | B) is equivalent to X & A | X & B, we can transform intersection // types with union type constituents into equivalent union types with intersection type constituents and @@ -8468,6 +8489,9 @@ namespace ts { includes & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol) { removeRedundantPrimitiveTypes(typeSet, includes); } + if (includes & TypeFlags.UnionOfUnitTypes) { + intersectUnionsOfUnitTypes(typeSet); + } if (includes & TypeFlags.EmptyObject && !(includes & TypeFlags.Object)) { typeSet.push(emptyObjectType); } @@ -13234,7 +13258,7 @@ namespace ts { if (type.flags & TypeFlags.Union) { const types = (type).types; const filtered = filter(types, f); - return filtered === types ? type : getUnionTypeFromSortedList(filtered); + return filtered === types ? type : getUnionTypeFromSortedList(filtered, type.flags & TypeFlags.UnionOfUnitTypes); } return f(type) ? type : neverType; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4b2a7c080d216..84844c437aa34 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3674,6 +3674,8 @@ namespace ts { ContainsAnyFunctionType = 1 << 26, // Type is or contains the anyFunctionType NonPrimitive = 1 << 27, // intrinsic object type /* @internal */ + UnionOfUnitTypes = 1 << 28, // Type is union of unit types + /* @internal */ GenericMappedType = 1 << 29, // Flag used by maybeTypeOfKind /* @internal */ @@ -3711,6 +3713,8 @@ namespace ts { Narrowable = Any | StructuredOrInstantiable | StringLike | NumberLike | BooleanLike | ESSymbol | UniqueESSymbol | NonPrimitive, NotUnionOrUnit = Any | ESSymbol | Object | NonPrimitive, /* @internal */ + NotUnit = Any | String | Number | Boolean | Enum | ESSymbol | Void | Never | StructuredOrInstantiable, + /* @internal */ RequiresWidening = ContainsWideningType | ContainsObjectLiteral, /* @internal */ PropagatingFlags = ContainsWideningType | ContainsObjectLiteral | ContainsAnyFunctionType, From 1c3dbd4f4b135f4153891f56482eeb111c3fa555 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Tue, 15 May 2018 12:34:29 -0700 Subject: [PATCH 2/4] Add regression test --- .../compiler/intersectionsOfLargeUnions.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/cases/compiler/intersectionsOfLargeUnions.ts diff --git a/tests/cases/compiler/intersectionsOfLargeUnions.ts b/tests/cases/compiler/intersectionsOfLargeUnions.ts new file mode 100644 index 0000000000000..1c8949873f44e --- /dev/null +++ b/tests/cases/compiler/intersectionsOfLargeUnions.ts @@ -0,0 +1,27 @@ +// @strict: true + +// Repro from #23977 + +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]; + } +} From 8b6e85347d4d8c9e84261107981665c6688d1dbf Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Tue, 15 May 2018 12:34:41 -0700 Subject: [PATCH 3/4] Accept new baselines --- .../intersectionsOfLargeUnions.errors.txt | 35 ++++++ .../reference/intersectionsOfLargeUnions.js | 51 ++++++++ .../intersectionsOfLargeUnions.symbols | 95 +++++++++++++++ .../intersectionsOfLargeUnions.types | 110 ++++++++++++++++++ 4 files changed, 291 insertions(+) create mode 100644 tests/baselines/reference/intersectionsOfLargeUnions.errors.txt create mode 100644 tests/baselines/reference/intersectionsOfLargeUnions.js create mode 100644 tests/baselines/reference/intersectionsOfLargeUnions.symbols create mode 100644 tests/baselines/reference/intersectionsOfLargeUnions.types diff --git a/tests/baselines/reference/intersectionsOfLargeUnions.errors.txt b/tests/baselines/reference/intersectionsOfLargeUnions.errors.txt new file mode 100644 index 0000000000000..b5911f646c7be --- /dev/null +++ b/tests/baselines/reference/intersectionsOfLargeUnions.errors.txt @@ -0,0 +1,35 @@ +tests/cases/compiler/intersectionsOfLargeUnions.ts(21,15): error TS2536: Type 'T' cannot be used to index type 'HTMLElementTagNameMap'. +tests/cases/compiler/intersectionsOfLargeUnions.ts(21,15): error TS2536: Type 'P' cannot be used to index type 'HTMLElementTagNameMap[T]'. + + +==== tests/cases/compiler/intersectionsOfLargeUnions.ts (2 errors) ==== + // Repro from #23977 + + 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/intersectionsOfLargeUnions.js b/tests/baselines/reference/intersectionsOfLargeUnions.js new file mode 100644 index 0000000000000..bd3b98daf01ab --- /dev/null +++ b/tests/baselines/reference/intersectionsOfLargeUnions.js @@ -0,0 +1,51 @@ +//// [intersectionsOfLargeUnions.ts] +// Repro from #23977 + +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]; + } +} + + +//// [intersectionsOfLargeUnions.js] +"use strict"; +// Repro from #23977 +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/intersectionsOfLargeUnions.symbols b/tests/baselines/reference/intersectionsOfLargeUnions.symbols new file mode 100644 index 0000000000000..02bcce616094d --- /dev/null +++ b/tests/baselines/reference/intersectionsOfLargeUnions.symbols @@ -0,0 +1,95 @@ +=== tests/cases/compiler/intersectionsOfLargeUnions.ts === +// Repro from #23977 + +export function assertIsElement(node: Node | null): node is Element { +>assertIsElement : Symbol(assertIsElement, Decl(intersectionsOfLargeUnions.ts, 0, 0)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 2, 32)) +>Node : Symbol(Node, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 2, 32)) +>Element : Symbol(Element, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) + + let nodeType = node === null ? null : node.nodeType; +>nodeType : Symbol(nodeType, Decl(intersectionsOfLargeUnions.ts, 3, 7)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 2, 32)) +>node.nodeType : Symbol(Node.nodeType, Decl(lib.d.ts, --, --)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 2, 32)) +>nodeType : Symbol(Node.nodeType, Decl(lib.d.ts, --, --)) + + return nodeType === 1; +>nodeType : Symbol(nodeType, Decl(intersectionsOfLargeUnions.ts, 3, 7)) +} + +export function assertNodeTagName< +>assertNodeTagName : Symbol(assertNodeTagName, Decl(intersectionsOfLargeUnions.ts, 5, 1)) + + T extends keyof ElementTagNameMap, +>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 7, 34)) +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.d.ts, --, --)) + + U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U { +>U : Symbol(U, Decl(intersectionsOfLargeUnions.ts, 8, 38)) +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.d.ts, --, --)) +>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 7, 34)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 9, 36)) +>Node : Symbol(Node, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) +>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions.ts, 9, 54)) +>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 7, 34)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 9, 36)) +>U : Symbol(U, Decl(intersectionsOfLargeUnions.ts, 8, 38)) + + if (assertIsElement(node)) { +>assertIsElement : Symbol(assertIsElement, Decl(intersectionsOfLargeUnions.ts, 0, 0)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 9, 36)) + + const nodeTagName = node.tagName.toLowerCase(); +>nodeTagName : Symbol(nodeTagName, Decl(intersectionsOfLargeUnions.ts, 11, 13)) +>node.tagName.toLowerCase : Symbol(String.toLowerCase, Decl(lib.d.ts, --, --)) +>node.tagName : Symbol(Element.tagName, Decl(lib.d.ts, --, --)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 9, 36)) +>tagName : Symbol(Element.tagName, Decl(lib.d.ts, --, --)) +>toLowerCase : Symbol(String.toLowerCase, Decl(lib.d.ts, --, --)) + + return nodeTagName === tagName; +>nodeTagName : Symbol(nodeTagName, Decl(intersectionsOfLargeUnions.ts, 11, 13)) +>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions.ts, 9, 54)) + } + return false; +} + +export function assertNodeProperty< +>assertNodeProperty : Symbol(assertNodeProperty, Decl(intersectionsOfLargeUnions.ts, 15, 1)) + + T extends keyof ElementTagNameMap, +>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 17, 35)) +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.d.ts, --, --)) + + P extends keyof ElementTagNameMap[T], +>P : Symbol(P, Decl(intersectionsOfLargeUnions.ts, 18, 38)) +>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.d.ts, --, --)) +>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 17, 35)) + + V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) { +>V : Symbol(V, Decl(intersectionsOfLargeUnions.ts, 19, 41)) +>HTMLElementTagNameMap : Symbol(HTMLElementTagNameMap, Decl(lib.d.ts, --, --)) +>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 17, 35)) +>P : Symbol(P, Decl(intersectionsOfLargeUnions.ts, 18, 38)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 20, 43)) +>Node : Symbol(Node, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) +>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions.ts, 20, 61)) +>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 17, 35)) +>prop : Symbol(prop, Decl(intersectionsOfLargeUnions.ts, 20, 73)) +>P : Symbol(P, Decl(intersectionsOfLargeUnions.ts, 18, 38)) +>value : Symbol(value, Decl(intersectionsOfLargeUnions.ts, 20, 82)) +>V : Symbol(V, Decl(intersectionsOfLargeUnions.ts, 19, 41)) + + if (assertNodeTagName(node, tagName)) { +>assertNodeTagName : Symbol(assertNodeTagName, Decl(intersectionsOfLargeUnions.ts, 5, 1)) +>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 20, 43)) +>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions.ts, 20, 61)) + + node[prop]; +>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 20, 43)) +>prop : Symbol(prop, Decl(intersectionsOfLargeUnions.ts, 20, 73)) + } +} + diff --git a/tests/baselines/reference/intersectionsOfLargeUnions.types b/tests/baselines/reference/intersectionsOfLargeUnions.types new file mode 100644 index 0000000000000..4deef4a4ecf88 --- /dev/null +++ b/tests/baselines/reference/intersectionsOfLargeUnions.types @@ -0,0 +1,110 @@ +=== tests/cases/compiler/intersectionsOfLargeUnions.ts === +// Repro from #23977 + +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 + } +} + From 027829fbcd0a84172ab4a36d60a0ac915ca7ea09 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Wed, 16 May 2018 16:26:37 -0700 Subject: [PATCH 4/4] Properly handle edge cases --- src/compiler/checker.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 705ecea6b5ec1..864ce12a6f9b7 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8457,9 +8457,11 @@ namespace ts { } i--; } - if (intersection !== unionType.types) { - types[unionIndex] = getUnionTypeFromSortedList(intersection, unionType.flags & TypeFlags.UnionOfUnitTypes); + if (intersection === unionType.types) { + return false; } + types[unionIndex] = getUnionTypeFromSortedList(intersection, unionType.flags & TypeFlags.UnionOfUnitTypes); + return true; } // We normalize combinations of intersection and union types based on the distributive property of the '&' @@ -8489,9 +8491,6 @@ namespace ts { includes & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol) { removeRedundantPrimitiveTypes(typeSet, includes); } - if (includes & TypeFlags.UnionOfUnitTypes) { - intersectUnionsOfUnitTypes(typeSet); - } if (includes & TypeFlags.EmptyObject && !(includes & TypeFlags.Object)) { typeSet.push(emptyObjectType); } @@ -8499,6 +8498,12 @@ namespace ts { return typeSet[0]; } if (includes & TypeFlags.Union) { + if (includes & TypeFlags.UnionOfUnitTypes && intersectUnionsOfUnitTypes(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. + return getIntersectionType(typeSet, aliasSymbol, aliasTypeArguments); + } // 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);