Skip to content

Commit 3fc3df3

Browse files
authored
Merge pull request #24137 from Microsoft/fix23977
Optimize intersections of unions of unit types
2 parents 3fc727b + 027829f commit 3fc3df3

7 files changed

+355
-4
lines changed

src/compiler/checker.ts

+33-4
Original file line numberDiff line numberDiff line change
@@ -8400,7 +8400,7 @@ namespace ts {
84008400
includes & TypeFlags.Undefined ? includes & TypeFlags.NonWideningType ? undefinedType : undefinedWideningType :
84018401
neverType;
84028402
}
8403-
return getUnionTypeFromSortedList(typeSet, aliasSymbol, aliasTypeArguments);
8403+
return getUnionTypeFromSortedList(typeSet, includes & TypeFlags.NotUnit ? 0 : TypeFlags.UnionOfUnitTypes, aliasSymbol, aliasTypeArguments);
84048404
}
84058405

84068406
function getUnionTypePredicate(signatures: ReadonlyArray<Signature>): TypePredicate {
@@ -8440,7 +8440,7 @@ namespace ts {
84408440
}
84418441

84428442
// This function assumes the constituent type list is sorted and deduplicated.
8443-
function getUnionTypeFromSortedList(types: Type[], aliasSymbol?: Symbol, aliasTypeArguments?: Type[]): Type {
8443+
function getUnionTypeFromSortedList(types: Type[], unionOfUnitTypes: TypeFlags, aliasSymbol?: Symbol, aliasTypeArguments?: Type[]): Type {
84448444
if (types.length === 0) {
84458445
return neverType;
84468446
}
@@ -8451,7 +8451,7 @@ namespace ts {
84518451
let type = unionTypes.get(id);
84528452
if (!type) {
84538453
const propagatedFlags = getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable);
8454-
type = <UnionType>createType(TypeFlags.Union | propagatedFlags);
8454+
type = <UnionType>createType(TypeFlags.Union | propagatedFlags | unionOfUnitTypes);
84558455
unionTypes.set(id, type);
84568456
type.types = types;
84578457
/*
@@ -8523,6 +8523,29 @@ namespace ts {
85238523
}
85248524
}
85258525

8526+
// When intersecting unions of unit types we can simply intersect based on type identity.
8527+
// Here we remove all unions of unit types from the given list and replace them with a
8528+
// a single union containing an intersection of the unit types.
8529+
function intersectUnionsOfUnitTypes(types: Type[]) {
8530+
const unionIndex = findIndex(types, t => (t.flags & TypeFlags.UnionOfUnitTypes) !== 0);
8531+
const unionType = <UnionType>types[unionIndex];
8532+
let intersection = unionType.types;
8533+
let i = types.length - 1;
8534+
while (i > unionIndex) {
8535+
const t = types[i];
8536+
if (t.flags & TypeFlags.UnionOfUnitTypes) {
8537+
intersection = filter(intersection, u => containsType((<UnionType>t).types, u));
8538+
orderedRemoveItemAt(types, i);
8539+
}
8540+
i--;
8541+
}
8542+
if (intersection === unionType.types) {
8543+
return false;
8544+
}
8545+
types[unionIndex] = getUnionTypeFromSortedList(intersection, unionType.flags & TypeFlags.UnionOfUnitTypes);
8546+
return true;
8547+
}
8548+
85268549
// We normalize combinations of intersection and union types based on the distributive property of the '&'
85278550
// operator. Specifically, because X & (A | B) is equivalent to X & A | X & B, we can transform intersection
85288551
// types with union type constituents into equivalent union types with intersection type constituents and
@@ -8557,6 +8580,12 @@ namespace ts {
85578580
return typeSet[0];
85588581
}
85598582
if (includes & TypeFlags.Union) {
8583+
if (includes & TypeFlags.UnionOfUnitTypes && intersectUnionsOfUnitTypes(typeSet)) {
8584+
// When the intersection creates a reduced set (which might mean that *all* union types have
8585+
// disappeared), we restart the operation to get a new set of combined flags. Once we have
8586+
// reduced we'll never reduce again, so this occurs at most once.
8587+
return getIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
8588+
}
85608589
// We are attempting to construct a type of the form X & (A | B) & Y. Transform this into a type of
85618590
// the form X & A & Y | X & B & Y and recursively reduce until no union type constituents remain.
85628591
const unionIndex = findIndex(typeSet, t => (t.flags & TypeFlags.Union) !== 0);
@@ -13331,7 +13360,7 @@ namespace ts {
1333113360
if (type.flags & TypeFlags.Union) {
1333213361
const types = (<UnionType>type).types;
1333313362
const filtered = filter(types, f);
13334-
return filtered === types ? type : getUnionTypeFromSortedList(filtered);
13363+
return filtered === types ? type : getUnionTypeFromSortedList(filtered, type.flags & TypeFlags.UnionOfUnitTypes);
1333513364
}
1333613365
return f(type) ? type : neverType;
1333713366
}

src/compiler/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -3692,6 +3692,8 @@ namespace ts {
36923692
ContainsAnyFunctionType = 1 << 26, // Type is or contains the anyFunctionType
36933693
NonPrimitive = 1 << 27, // intrinsic object type
36943694
/* @internal */
3695+
UnionOfUnitTypes = 1 << 28, // Type is union of unit types
3696+
/* @internal */
36953697
GenericMappedType = 1 << 29, // Flag used by maybeTypeOfKind
36963698

36973699
/* @internal */
@@ -3729,6 +3731,8 @@ namespace ts {
37293731
Narrowable = Any | StructuredOrInstantiable | StringLike | NumberLike | BooleanLike | ESSymbol | UniqueESSymbol | NonPrimitive,
37303732
NotUnionOrUnit = Any | ESSymbol | Object | NonPrimitive,
37313733
/* @internal */
3734+
NotUnit = Any | String | Number | Boolean | Enum | ESSymbol | Void | Never | StructuredOrInstantiable,
3735+
/* @internal */
37323736
RequiresWidening = ContainsWideningType | ContainsObjectLiteral,
37333737
/* @internal */
37343738
PropagatingFlags = ContainsWideningType | ContainsObjectLiteral | ContainsAnyFunctionType,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
tests/cases/compiler/intersectionsOfLargeUnions.ts(21,15): error TS2536: Type 'T' cannot be used to index type 'HTMLElementTagNameMap'.
2+
tests/cases/compiler/intersectionsOfLargeUnions.ts(21,15): error TS2536: Type 'P' cannot be used to index type 'HTMLElementTagNameMap[T]'.
3+
4+
5+
==== tests/cases/compiler/intersectionsOfLargeUnions.ts (2 errors) ====
6+
// Repro from #23977
7+
8+
export function assertIsElement(node: Node | null): node is Element {
9+
let nodeType = node === null ? null : node.nodeType;
10+
return nodeType === 1;
11+
}
12+
13+
export function assertNodeTagName<
14+
T extends keyof ElementTagNameMap,
15+
U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U {
16+
if (assertIsElement(node)) {
17+
const nodeTagName = node.tagName.toLowerCase();
18+
return nodeTagName === tagName;
19+
}
20+
return false;
21+
}
22+
23+
export function assertNodeProperty<
24+
T extends keyof ElementTagNameMap,
25+
P extends keyof ElementTagNameMap[T],
26+
V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) {
27+
~~~~~~~~~~~~~~~~~~~~~~~~
28+
!!! error TS2536: Type 'T' cannot be used to index type 'HTMLElementTagNameMap'.
29+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
30+
!!! error TS2536: Type 'P' cannot be used to index type 'HTMLElementTagNameMap[T]'.
31+
if (assertNodeTagName(node, tagName)) {
32+
node[prop];
33+
}
34+
}
35+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//// [intersectionsOfLargeUnions.ts]
2+
// Repro from #23977
3+
4+
export function assertIsElement(node: Node | null): node is Element {
5+
let nodeType = node === null ? null : node.nodeType;
6+
return nodeType === 1;
7+
}
8+
9+
export function assertNodeTagName<
10+
T extends keyof ElementTagNameMap,
11+
U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U {
12+
if (assertIsElement(node)) {
13+
const nodeTagName = node.tagName.toLowerCase();
14+
return nodeTagName === tagName;
15+
}
16+
return false;
17+
}
18+
19+
export function assertNodeProperty<
20+
T extends keyof ElementTagNameMap,
21+
P extends keyof ElementTagNameMap[T],
22+
V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) {
23+
if (assertNodeTagName(node, tagName)) {
24+
node[prop];
25+
}
26+
}
27+
28+
29+
//// [intersectionsOfLargeUnions.js]
30+
"use strict";
31+
// Repro from #23977
32+
exports.__esModule = true;
33+
function assertIsElement(node) {
34+
var nodeType = node === null ? null : node.nodeType;
35+
return nodeType === 1;
36+
}
37+
exports.assertIsElement = assertIsElement;
38+
function assertNodeTagName(node, tagName) {
39+
if (assertIsElement(node)) {
40+
var nodeTagName = node.tagName.toLowerCase();
41+
return nodeTagName === tagName;
42+
}
43+
return false;
44+
}
45+
exports.assertNodeTagName = assertNodeTagName;
46+
function assertNodeProperty(node, tagName, prop, value) {
47+
if (assertNodeTagName(node, tagName)) {
48+
node[prop];
49+
}
50+
}
51+
exports.assertNodeProperty = assertNodeProperty;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
=== tests/cases/compiler/intersectionsOfLargeUnions.ts ===
2+
// Repro from #23977
3+
4+
export function assertIsElement(node: Node | null): node is Element {
5+
>assertIsElement : Symbol(assertIsElement, Decl(intersectionsOfLargeUnions.ts, 0, 0))
6+
>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 2, 32))
7+
>Node : Symbol(Node, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
8+
>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 2, 32))
9+
>Element : Symbol(Element, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
10+
11+
let nodeType = node === null ? null : node.nodeType;
12+
>nodeType : Symbol(nodeType, Decl(intersectionsOfLargeUnions.ts, 3, 7))
13+
>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 2, 32))
14+
>node.nodeType : Symbol(Node.nodeType, Decl(lib.d.ts, --, --))
15+
>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 2, 32))
16+
>nodeType : Symbol(Node.nodeType, Decl(lib.d.ts, --, --))
17+
18+
return nodeType === 1;
19+
>nodeType : Symbol(nodeType, Decl(intersectionsOfLargeUnions.ts, 3, 7))
20+
}
21+
22+
export function assertNodeTagName<
23+
>assertNodeTagName : Symbol(assertNodeTagName, Decl(intersectionsOfLargeUnions.ts, 5, 1))
24+
25+
T extends keyof ElementTagNameMap,
26+
>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 7, 34))
27+
>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.d.ts, --, --))
28+
29+
U extends ElementTagNameMap[T]>(node: Node | null, tagName: T): node is U {
30+
>U : Symbol(U, Decl(intersectionsOfLargeUnions.ts, 8, 38))
31+
>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.d.ts, --, --))
32+
>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 7, 34))
33+
>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 9, 36))
34+
>Node : Symbol(Node, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
35+
>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions.ts, 9, 54))
36+
>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 7, 34))
37+
>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 9, 36))
38+
>U : Symbol(U, Decl(intersectionsOfLargeUnions.ts, 8, 38))
39+
40+
if (assertIsElement(node)) {
41+
>assertIsElement : Symbol(assertIsElement, Decl(intersectionsOfLargeUnions.ts, 0, 0))
42+
>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 9, 36))
43+
44+
const nodeTagName = node.tagName.toLowerCase();
45+
>nodeTagName : Symbol(nodeTagName, Decl(intersectionsOfLargeUnions.ts, 11, 13))
46+
>node.tagName.toLowerCase : Symbol(String.toLowerCase, Decl(lib.d.ts, --, --))
47+
>node.tagName : Symbol(Element.tagName, Decl(lib.d.ts, --, --))
48+
>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 9, 36))
49+
>tagName : Symbol(Element.tagName, Decl(lib.d.ts, --, --))
50+
>toLowerCase : Symbol(String.toLowerCase, Decl(lib.d.ts, --, --))
51+
52+
return nodeTagName === tagName;
53+
>nodeTagName : Symbol(nodeTagName, Decl(intersectionsOfLargeUnions.ts, 11, 13))
54+
>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions.ts, 9, 54))
55+
}
56+
return false;
57+
}
58+
59+
export function assertNodeProperty<
60+
>assertNodeProperty : Symbol(assertNodeProperty, Decl(intersectionsOfLargeUnions.ts, 15, 1))
61+
62+
T extends keyof ElementTagNameMap,
63+
>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 17, 35))
64+
>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.d.ts, --, --))
65+
66+
P extends keyof ElementTagNameMap[T],
67+
>P : Symbol(P, Decl(intersectionsOfLargeUnions.ts, 18, 38))
68+
>ElementTagNameMap : Symbol(ElementTagNameMap, Decl(lib.d.ts, --, --))
69+
>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 17, 35))
70+
71+
V extends HTMLElementTagNameMap[T][P]>(node: Node | null, tagName: T, prop: P, value: V) {
72+
>V : Symbol(V, Decl(intersectionsOfLargeUnions.ts, 19, 41))
73+
>HTMLElementTagNameMap : Symbol(HTMLElementTagNameMap, Decl(lib.d.ts, --, --))
74+
>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 17, 35))
75+
>P : Symbol(P, Decl(intersectionsOfLargeUnions.ts, 18, 38))
76+
>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 20, 43))
77+
>Node : Symbol(Node, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
78+
>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions.ts, 20, 61))
79+
>T : Symbol(T, Decl(intersectionsOfLargeUnions.ts, 17, 35))
80+
>prop : Symbol(prop, Decl(intersectionsOfLargeUnions.ts, 20, 73))
81+
>P : Symbol(P, Decl(intersectionsOfLargeUnions.ts, 18, 38))
82+
>value : Symbol(value, Decl(intersectionsOfLargeUnions.ts, 20, 82))
83+
>V : Symbol(V, Decl(intersectionsOfLargeUnions.ts, 19, 41))
84+
85+
if (assertNodeTagName(node, tagName)) {
86+
>assertNodeTagName : Symbol(assertNodeTagName, Decl(intersectionsOfLargeUnions.ts, 5, 1))
87+
>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 20, 43))
88+
>tagName : Symbol(tagName, Decl(intersectionsOfLargeUnions.ts, 20, 61))
89+
90+
node[prop];
91+
>node : Symbol(node, Decl(intersectionsOfLargeUnions.ts, 20, 43))
92+
>prop : Symbol(prop, Decl(intersectionsOfLargeUnions.ts, 20, 73))
93+
}
94+
}
95+

0 commit comments

Comments
 (0)