Skip to content

Commit ecb6eb2

Browse files
authored
Divide-and-conquer strategy for intersections of unions (#57871)
1 parent 03c4b35 commit ecb6eb2

7 files changed

+720
-2
lines changed

src/compiler/checker.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17763,6 +17763,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1776317763
removeFromEach(typeSet, TypeFlags.Null);
1776417764
result = getUnionType([getIntersectionType(typeSet), nullType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
1776517765
}
17766+
else if (typeSet.length >= 4) {
17767+
// When we have four or more constituents, some of which are unions, we employ a "divide and conquer" strategy
17768+
// where A & B & C & D is processed as (A & B) & (C & D). Since intersections of unions often produce far smaller
17769+
// unions of intersections than the full cartesian product (due to some intersections becoming `never`), this can
17770+
// dramatically reduce the overall work.
17771+
const middle = Math.floor(typeSet.length / 2);
17772+
result = getIntersectionType([getIntersectionType(typeSet.slice(0, middle)), getIntersectionType(typeSet.slice(middle))], aliasSymbol, aliasTypeArguments);
17773+
}
1776617774
else {
1776717775
// We are attempting to construct a type of the form X & (A | B) & (C | D). Transform this into a type of
1776817776
// the form X & A & C | X & A & D | X & B & C | X & B & D. If the estimated size of the resulting union type

tests/baselines/reference/divideAndConquerIntersections.symbols

Lines changed: 361 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
//// [tests/cases/compiler/divideAndConquerIntersections.ts] ////
2+
3+
=== Performance Stats ===
4+
Assignability cache: 100 / 100 (nearest 100)
5+
Type Count: 2,800 / 2,900 (nearest 100)
6+
Instantiation count: 7,500 / 7,500 (nearest 500)
7+
Symbol count: 26,000 / 26,000 (nearest 500)
8+
9+
=== divideAndConquerIntersections.ts ===
10+
type QQ<T extends string[]> =
11+
>QQ : QQ<T>
12+
13+
& ("a" | T[0])
14+
& ("b" | T[1])
15+
& ("c" | T[2])
16+
& ("d" | T[3])
17+
& ("e" | T[4])
18+
& ("f" | T[5])
19+
& ("g" | T[6])
20+
& ("h" | T[7])
21+
& ("i" | T[8])
22+
& ("j" | T[9])
23+
& ("k" | T[10])
24+
& ("l" | T[11])
25+
& ("m" | T[12])
26+
& ("n" | T[13])
27+
& ("q" | T[14])
28+
& ("p" | T[15])
29+
& ("q" | T[16])
30+
& ("r" | T[17])
31+
& ("s" | T[18])
32+
& ("t" | T[19]);
33+
34+
// Repro from #57863
35+
36+
export interface Update {
37+
update_id: number;
38+
>update_id : number
39+
40+
message?: { message: string };
41+
>message : { message: string; } | undefined
42+
>message : string
43+
44+
edited_message?: { edited_message: string };
45+
>edited_message : { edited_message: string; } | undefined
46+
>edited_message : string
47+
48+
channel_post?: { channel_post: string };
49+
>channel_post : { channel_post: string; } | undefined
50+
>channel_post : string
51+
52+
edited_channel_post?: { edited_channel_post: string };
53+
>edited_channel_post : { edited_channel_post: string; } | undefined
54+
>edited_channel_post : string
55+
56+
message_reaction?: { message_reaction: string };
57+
>message_reaction : { message_reaction: string; } | undefined
58+
>message_reaction : string
59+
60+
message_reaction_count?: { message_reaction_count: string };
61+
>message_reaction_count : { message_reaction_count: string; } | undefined
62+
>message_reaction_count : string
63+
64+
inline_query?: { inline_query: string };
65+
>inline_query : { inline_query: string; } | undefined
66+
>inline_query : string
67+
68+
chosen_inline_result?: { chosen_inline_result: string };
69+
>chosen_inline_result : { chosen_inline_result: string; } | undefined
70+
>chosen_inline_result : string
71+
72+
callback_query?: { callback_query: string };
73+
>callback_query : { callback_query: string; } | undefined
74+
>callback_query : string
75+
76+
shipping_query?: { shipping_query: string };
77+
>shipping_query : { shipping_query: string; } | undefined
78+
>shipping_query : string
79+
80+
pre_checkout_query?: { pre_checkout_query: string };
81+
>pre_checkout_query : { pre_checkout_query: string; } | undefined
82+
>pre_checkout_query : string
83+
84+
poll?: { poll: string };
85+
>poll : { poll: string; } | undefined
86+
>poll : string
87+
88+
poll_answer?: { poll_answer: string };
89+
>poll_answer : { poll_answer: string; } | undefined
90+
>poll_answer : string
91+
92+
my_chat_member?: { my_chat_member: string };
93+
>my_chat_member : { my_chat_member: string; } | undefined
94+
>my_chat_member : string
95+
96+
chat_member?: { chat_member: string };
97+
>chat_member : { chat_member: string; } | undefined
98+
>chat_member : string
99+
100+
chat_join_request?: { chat_join_request: string };
101+
>chat_join_request : { chat_join_request: string; } | undefined
102+
>chat_join_request : string
103+
104+
chat_boost?: { chat_boost: string };
105+
>chat_boost : { chat_boost: string; } | undefined
106+
>chat_boost : string
107+
108+
removed_chat_boost?: { removed_chat_boost: string };
109+
>removed_chat_boost : { removed_chat_boost: string; } | undefined
110+
>removed_chat_boost : string
111+
}
112+
113+
type FilterFunction<U extends Update, V extends U> = (up: U) => up is V;
114+
>FilterFunction : FilterFunction<U, V>
115+
>up : U
116+
117+
export function matchFilter<U extends Update, Q extends FilterQuery>(
118+
>matchFilter : <U extends Update, Q extends "message" | "edited_message" | "channel_post" | "edited_channel_post" | "message_reaction" | "message_reaction_count" | "inline_query" | "chosen_inline_result" | "callback_query" | "shipping_query" | "pre_checkout_query" | "poll" | "poll_answer" | "my_chat_member" | "chat_member" | "chat_join_request" | "chat_boost" | "removed_chat_boost">(filter: Q | Q[]) => FilterFunction<U, Filter<U, Q>>
119+
120+
filter: Q | Q[],
121+
>filter : Q | Q[]
122+
123+
): FilterFunction<U, Filter<U, Q>> {
124+
// ^ errors out
125+
console.log("Matching", filter);
126+
>console.log("Matching", filter) : void
127+
>console.log : (...data: any[]) => void
128+
>console : Console
129+
>log : (...data: any[]) => void
130+
>"Matching" : "Matching"
131+
>filter : "message" | "edited_message" | "channel_post" | "edited_channel_post" | "message_reaction" | "message_reaction_count" | "inline_query" | "chosen_inline_result" | "callback_query" | "shipping_query" | "pre_checkout_query" | "poll" | "poll_answer" | "my_chat_member" | "chat_member" | "chat_join_request" | "chat_boost" | "removed_chat_boost" | Q[]
132+
133+
return (up: U): up is Filter<U, Q> => !!up;
134+
>(up: U): up is Filter<U, Q> => !!up : (up: U) => up is PerformQuery<U, Combine<L1Fragment<Q>, Q>>
135+
>up : U
136+
>!!up : true
137+
>!up : false
138+
>up : U
139+
}
140+
141+
/** All valid filter queries (every update key except update_id) */
142+
export type FilterQuery = keyof Omit<Update, "update_id">;
143+
>FilterQuery : "message" | "edited_message" | "channel_post" | "edited_channel_post" | "message_reaction" | "message_reaction_count" | "inline_query" | "chosen_inline_result" | "callback_query" | "shipping_query" | "pre_checkout_query" | "poll" | "poll_answer" | "my_chat_member" | "chat_member" | "chat_join_request" | "chat_boost" | "removed_chat_boost"
144+
145+
/** Narrow down an update object based on a filter query */
146+
export type Filter<U extends Update, Q extends FilterQuery> = PerformQuery<
147+
>Filter : Filter<U, Q>
148+
149+
U,
150+
RunQuery<Q>
151+
>;
152+
153+
// generate an object structure that can be intersected with updates to narrow them down
154+
type RunQuery<Q extends string> = Combine<L1Fragment<Q>, Q>;
155+
>RunQuery : RunQuery<Q>
156+
157+
// maps each part of the filter query to Record<"key", object>
158+
type L1Fragment<Q extends string> = Q extends unknown ? Record<Q, object>
159+
>L1Fragment : L1Fragment<Q>
160+
161+
: never;
162+
// define all other fields from query as keys with value `undefined`
163+
type Combine<U, K extends string> = U extends unknown
164+
>Combine : Combine<U, K>
165+
166+
? U & Partial<Record<Exclude<K, keyof U>, undefined>>
167+
: never;
168+
169+
// apply a query result by intersecting it with update,
170+
// and then using these values to override the actual update
171+
type PerformQuery<U extends Update, R extends object> = R extends unknown
172+
>PerformQuery : PerformQuery<U, R>
173+
174+
? FilteredEvent<U, Update & R>
175+
: never;
176+
177+
// narrow down an update by intersecting it with a different update
178+
type FilteredEvent<E extends Update, U extends Update> =
179+
>FilteredEvent : FilteredEvent<E, U>
180+
181+
& E
182+
& Omit<U, "update_id">;
183+
184+
type Middleware<U extends Update> = (ctx: U) => unknown | Promise<unknown>;
185+
>Middleware : Middleware<U>
186+
>ctx : U
187+
188+
class EventHub<U extends Update> {
189+
>EventHub : EventHub<U>
190+
191+
use(...middleware: Array<Middleware<U>>): EventHub<U> {
192+
>use : (...middleware: Array<Middleware<U>>) => EventHub<U>
193+
>middleware : Middleware<U>[]
194+
195+
console.log("Adding", middleware.length, "generic handlers");
196+
>console.log("Adding", middleware.length, "generic handlers") : void
197+
>console.log : (...data: any[]) => void
198+
>console : Console
199+
>log : (...data: any[]) => void
200+
>"Adding" : "Adding"
201+
>middleware.length : number
202+
>middleware : Middleware<U>[]
203+
>length : number
204+
>"generic handlers" : "generic handlers"
205+
206+
return this;
207+
>this : this
208+
}
209+
on<Q extends FilterQuery>(
210+
>on : <Q extends "message" | "edited_message" | "channel_post" | "edited_channel_post" | "message_reaction" | "message_reaction_count" | "inline_query" | "chosen_inline_result" | "callback_query" | "shipping_query" | "pre_checkout_query" | "poll" | "poll_answer" | "my_chat_member" | "chat_member" | "chat_join_request" | "chat_boost" | "removed_chat_boost">(filter: Q | Q[], ...middleware: Array<Middleware<Filter<U, Q>>>) => EventHub<Filter<U, Q>>
211+
212+
filter: Q | Q[],
213+
>filter : Q | Q[]
214+
215+
...middleware: Array<Middleware<Filter<U, Q>>>
216+
>middleware : Middleware<PerformQuery<U, Combine<L1Fragment<Q>, Q>>>[]
217+
218+
// ^ errors out
219+
): EventHub<Filter<U, Q>> {
220+
console.log("Adding", middleware.length, "handlers for", filter);
221+
>console.log("Adding", middleware.length, "handlers for", filter) : void
222+
>console.log : (...data: any[]) => void
223+
>console : Console
224+
>log : (...data: any[]) => void
225+
>"Adding" : "Adding"
226+
>middleware.length : number
227+
>middleware : Middleware<PerformQuery<U, Combine<L1Fragment<Q>, Q>>>[]
228+
>length : number
229+
>"handlers for" : "handlers for"
230+
>filter : "message" | "edited_message" | "channel_post" | "edited_channel_post" | "message_reaction" | "message_reaction_count" | "inline_query" | "chosen_inline_result" | "callback_query" | "shipping_query" | "pre_checkout_query" | "poll" | "poll_answer" | "my_chat_member" | "chat_member" | "chat_join_request" | "chat_boost" | "removed_chat_boost" | Q[]
231+
232+
return new EventHub<Filter<U, Q>>();
233+
>new EventHub<Filter<U, Q>>() : EventHub<PerformQuery<U, Combine<L1Fragment<Q>, Q>>>
234+
>EventHub : typeof EventHub
235+
}
236+
}
237+

tests/baselines/reference/normalizedIntersectionTooComplex.types

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
//// [tests/cases/compiler/normalizedIntersectionTooComplex.ts] ////
22

3+
=== Performance Stats ===
4+
Assignability cache: 300 / 300 (nearest 100)
5+
Type Count: 1,500 / 1,500 (nearest 100)
6+
Instantiation count: 500 / 500 (nearest 500)
7+
Symbol count: 25,500 / 25,500 (nearest 500)
8+
39
=== normalizedIntersectionTooComplex.ts ===
410
// Repro from #30050
511

tests/baselines/reference/styledComponentsInstantiaionLimitNotReached.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
=== Performance Stats ===
44
Identity cache: 200 / 200 (nearest 100)
55
Assignability cache: 13,300 / 13,500 (nearest 100)
6-
Type Count: 27,200 / 27,600 (nearest 100)
6+
Type Count: 27,300 / 27,600 (nearest 100)
77
Instantiation count: 374,500 / 375,500 (nearest 500)
88
Symbol count: 92,000 / 92,000 (nearest 500)
99

tests/baselines/reference/templateLiteralTypes1.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
=== Performance Stats ===
44
Assignability cache: 900 / 900 (nearest 100)
5-
Type Count: 15,000 / 15,000 (nearest 100)
5+
Type Count: 16,100 / 16,100 (nearest 100)
66
Instantiation count: 3,500 / 3,500 (nearest 500)
77
Symbol count: 27,000 / 27,500 (nearest 500)
88

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// @strict: true
2+
// @noEmit: true
3+
4+
type QQ<T extends string[]> =
5+
& ("a" | T[0])
6+
& ("b" | T[1])
7+
& ("c" | T[2])
8+
& ("d" | T[3])
9+
& ("e" | T[4])
10+
& ("f" | T[5])
11+
& ("g" | T[6])
12+
& ("h" | T[7])
13+
& ("i" | T[8])
14+
& ("j" | T[9])
15+
& ("k" | T[10])
16+
& ("l" | T[11])
17+
& ("m" | T[12])
18+
& ("n" | T[13])
19+
& ("q" | T[14])
20+
& ("p" | T[15])
21+
& ("q" | T[16])
22+
& ("r" | T[17])
23+
& ("s" | T[18])
24+
& ("t" | T[19]);
25+
26+
// Repro from #57863
27+
28+
export interface Update {
29+
update_id: number;
30+
31+
message?: { message: string };
32+
edited_message?: { edited_message: string };
33+
channel_post?: { channel_post: string };
34+
edited_channel_post?: { edited_channel_post: string };
35+
message_reaction?: { message_reaction: string };
36+
message_reaction_count?: { message_reaction_count: string };
37+
inline_query?: { inline_query: string };
38+
chosen_inline_result?: { chosen_inline_result: string };
39+
callback_query?: { callback_query: string };
40+
shipping_query?: { shipping_query: string };
41+
pre_checkout_query?: { pre_checkout_query: string };
42+
poll?: { poll: string };
43+
poll_answer?: { poll_answer: string };
44+
my_chat_member?: { my_chat_member: string };
45+
chat_member?: { chat_member: string };
46+
chat_join_request?: { chat_join_request: string };
47+
chat_boost?: { chat_boost: string };
48+
removed_chat_boost?: { removed_chat_boost: string };
49+
}
50+
51+
type FilterFunction<U extends Update, V extends U> = (up: U) => up is V;
52+
53+
export function matchFilter<U extends Update, Q extends FilterQuery>(
54+
filter: Q | Q[],
55+
): FilterFunction<U, Filter<U, Q>> {
56+
// ^ errors out
57+
console.log("Matching", filter);
58+
return (up: U): up is Filter<U, Q> => !!up;
59+
}
60+
61+
/** All valid filter queries (every update key except update_id) */
62+
export type FilterQuery = keyof Omit<Update, "update_id">;
63+
64+
/** Narrow down an update object based on a filter query */
65+
export type Filter<U extends Update, Q extends FilterQuery> = PerformQuery<
66+
U,
67+
RunQuery<Q>
68+
>;
69+
70+
// generate an object structure that can be intersected with updates to narrow them down
71+
type RunQuery<Q extends string> = Combine<L1Fragment<Q>, Q>;
72+
73+
// maps each part of the filter query to Record<"key", object>
74+
type L1Fragment<Q extends string> = Q extends unknown ? Record<Q, object>
75+
: never;
76+
// define all other fields from query as keys with value `undefined`
77+
type Combine<U, K extends string> = U extends unknown
78+
? U & Partial<Record<Exclude<K, keyof U>, undefined>>
79+
: never;
80+
81+
// apply a query result by intersecting it with update,
82+
// and then using these values to override the actual update
83+
type PerformQuery<U extends Update, R extends object> = R extends unknown
84+
? FilteredEvent<U, Update & R>
85+
: never;
86+
87+
// narrow down an update by intersecting it with a different update
88+
type FilteredEvent<E extends Update, U extends Update> =
89+
& E
90+
& Omit<U, "update_id">;
91+
92+
type Middleware<U extends Update> = (ctx: U) => unknown | Promise<unknown>;
93+
class EventHub<U extends Update> {
94+
use(...middleware: Array<Middleware<U>>): EventHub<U> {
95+
console.log("Adding", middleware.length, "generic handlers");
96+
return this;
97+
}
98+
on<Q extends FilterQuery>(
99+
filter: Q | Q[],
100+
...middleware: Array<Middleware<Filter<U, Q>>>
101+
// ^ errors out
102+
): EventHub<Filter<U, Q>> {
103+
console.log("Adding", middleware.length, "handlers for", filter);
104+
return new EventHub<Filter<U, Q>>();
105+
}
106+
}

0 commit comments

Comments
 (0)