Skip to content

Commit 757bee7

Browse files
committed
When an intersection is going to produce an expression too complex error, eagerly perform reductions in a last-ditch effort to avoid the error
1 parent 4fc9c84 commit 757bee7

File tree

6 files changed

+742
-3
lines changed

6 files changed

+742
-3
lines changed

src/compiler/checker.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13728,7 +13728,7 @@ namespace ts {
1372813728
function getIntersectionType(types: readonly Type[], aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type {
1372913729
const typeMembershipMap: ESMap<string, Type> = new Map();
1373013730
const includes = addTypesToIntersection(typeMembershipMap, 0, types);
13731-
const typeSet: Type[] = arrayFrom(typeMembershipMap.values());
13731+
let typeSet: Type[] = arrayFrom(typeMembershipMap.values());
1373213732
// An intersection type is considered empty if it contains
1373313733
// the type never, or
1373413734
// more than one unit type or,
@@ -13789,6 +13789,41 @@ namespace ts {
1378913789
result = getUnionType([getIntersectionType(typeSet), nullType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
1379013790
}
1379113791
else {
13792+
let runningResult: Type | undefined;
13793+
const originalSet = typeSet;
13794+
if (typeSet.length > 2 && getCrossProductUnionSize(typeSet) >= 100000 && every(typeSet, t => !!(t.flags & TypeFlags.Union) || !!(t.flags & TypeFlags.Primitive))) {
13795+
// This type set is going to trigger an "expression too complex" error below. Rather than resort to that, as a last, best effort, when
13796+
// the intersection looks like (A | B | C) & (D | E | F) & (G | H | I) - in the general case, this can result in a massive resulting
13797+
// union, hence the check on the cross product size below, _however_ in some cases we can simplify the resulting type massively
13798+
// - if we can recognize that upfront, we can still allow the type to form without creating innumerable intermediate types.
13799+
// Specifically, in cases where almost all combinations are known to reduce to `never` (so the result is essentially sparse)
13800+
// and we can recognize that quickly, we can use a simplified result without checking the worst-case size.
13801+
// So we start with the assumption that the result _is_ sparse when the input looks like the above, and we assume the result
13802+
// will take the form (A & D & G) | (B & E & H) | (C & F & I). To validate this, we reduce left, first combining
13803+
// (A | B | C) & (D | E | F); if that combines into `(A & D) | (B & E) | (C & F)` like we want, which we make 9 intermediate
13804+
// types to check, we can then combine the reduced `(A & D) | (B & E) | (C & F)` with (G | H | I), which again takes 9 intermediate types
13805+
// to check, finally producing `(A & D & G) | (B & E & H) | (C & F & I)`. This required 18 intermediate types, while the standard method
13806+
// of expanding (A | B | C) & (D | E | F) & (G | H | I) would produce 27 types and then perform reduction on the result.
13807+
// By going elemnt-wise, and bailing if the result fails to reduce, we can allow these sparse expansions without doing undue work.
13808+
runningResult = typeSet[0];
13809+
for (let i = 1; i < typeSet.length; i++) {
13810+
// for intersection reduction, here we're considering `undefined & (A | B)` as `never`. (ie, we're disallowing branded primitives)
13811+
// This is relevant for, eg, when looking at `(HTMLElement | null) & (SVGElement | null) & ... & undefined` where _usually_
13812+
// we'd allow for tons of garbage intermediate types like `null & SVGElement` to exist; but nobody ever really actually _wants_
13813+
// that, IMO. Those types can still exist in the type system; just... not when working with unions and intersections with massive
13814+
// cross-product growth potential.
13815+
runningResult = typeSet[i].flags & TypeFlags.Primitive && everyType(runningResult, t => !!(t.flags & TypeFlags.Object)) ? neverType : getReducedType(intersectTypes(runningResult, typeSet[i]));
13816+
if (i === typeSet.length - 1 || isTypeAny(runningResult) || runningResult.flags & TypeFlags.Never) {
13817+
return runningResult;
13818+
}
13819+
if (!(runningResult.flags & TypeFlags.Union) || (runningResult as UnionType).types.length > typeSet.length) {
13820+
// save work done by the accumulated result thus far, even if we're bailing on the heuristic
13821+
// (it may have saved us enough work already that we're willing to work with the type now)
13822+
typeSet = typeSet.slice(i + 1);
13823+
break;
13824+
}
13825+
}
13826+
}
1379213827
// We are attempting to construct a type of the form X & (A | B) & (C | D). Transform this into a type of
1379313828
// the form X & A & C | X & A & D | X & B & C | X & B & D. If the estimated size of the resulting union type
1379413829
// exceeds 100000 constituents, report an error.
@@ -13798,8 +13833,8 @@ namespace ts {
1379813833
const constituents = getCrossProductIntersections(typeSet);
1379913834
// We attach a denormalized origin type when at least one constituent of the cross-product union is an
1380013835
// intersection (i.e. when the intersection didn't just reduce one or more unions to smaller unions).
13801-
const origin = some(constituents, t => !!(t.flags & TypeFlags.Intersection)) ? createOriginUnionOrIntersectionType(TypeFlags.Intersection, typeSet) : undefined;
13802-
result = getUnionType(constituents, UnionReduction.Literal, aliasSymbol, aliasTypeArguments, origin);
13836+
const origin = some(constituents, t => !!(t.flags & TypeFlags.Intersection)) ? createOriginUnionOrIntersectionType(TypeFlags.Intersection, originalSet) : undefined;
13837+
result = runningResult ? getIntersectionType([runningResult, getUnionType(constituents, UnionReduction.Literal)], aliasSymbol, aliasTypeArguments) : getUnionType(constituents, UnionReduction.Literal, aliasSymbol, aliasTypeArguments, origin);
1380313838
}
1380413839
}
1380513840
else {

tests/baselines/reference/jsxLargeRefUnion.errors.txt

Lines changed: 89 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//// [jsxLargeRefUnion.tsx]
2+
/// <reference path="/.lib/react16.d.ts" />
3+
4+
import * as React from "react";
5+
6+
const animated: {
7+
[Tag in keyof JSX.IntrinsicElements]: React.ForwardRefExoticComponent<
8+
React.ComponentPropsWithRef<Tag>
9+
>
10+
} = {} as any;
11+
12+
function makeAnimated<T extends React.ElementType<any>>(
13+
comp: T
14+
): React.ForwardRefExoticComponent<React.ComponentPropsWithRef<T>> {
15+
return null as any; // not important
16+
}
17+
18+
export interface UpgradedProps {
19+
show: boolean;
20+
}
21+
22+
export function test<P>(
23+
component: React.ComponentType<P> | keyof React.ReactHTML
24+
): React.ComponentType<P & UpgradedProps> {
25+
// changing to `const Comp: any` un-hangs tsserver
26+
const Comp =
27+
typeof component === "string"
28+
? animated[component]
29+
: makeAnimated(component);
30+
31+
return React.forwardRef<any, P & UpgradedProps>((props, ref) => {
32+
const { show, ...ownProps } = props;
33+
return show ? <Comp {...ownProps} ref={ref} /> : null; // ref as currently defined is expression-too-complex
34+
});
35+
}
36+
37+
type FixedRef<T> = string | null | React.RefObject<T> | { bivarianceHack(instance: T | null): any }["bivarianceHack"] & {current?: undefined};
38+
declare module "react" {
39+
interface DOMElement<P extends HTMLAttributes<T> | SVGAttributes<T>, T extends Element> extends ReactElement<P> {
40+
customRef: FixedRef<T>;
41+
}
42+
}
43+
interface ForwardCustomRefRenderFunction<T, P = {}> {
44+
(props: React.PropsWithChildren<P>, ref: FixedRef<T>): React.ReactElement<any> | null;
45+
displayName?: string;
46+
defaultProps?: never;
47+
propTypes?: never;
48+
}
49+
declare function forwardCustomRef<T, P = {}>(Component: ForwardCustomRefRenderFunction<T, P>): React.ComponentType<P & React.ClassAttributes<T>>;
50+
51+
export function test2<P>(
52+
component: React.ComponentType<P> | keyof React.ReactHTML
53+
): React.ComponentType<P & UpgradedProps> {
54+
// changing to `const Comp: any` un-hangs tsserver
55+
const Comp =
56+
typeof component === "string"
57+
? animated[component]
58+
: makeAnimated(component);
59+
60+
return forwardCustomRef<any, P & UpgradedProps>((props, ref) => {
61+
const { show, ...ownProps } = props;
62+
return show ? <Comp {...ownProps} customRef={ref} /> : null; // with the additional `current?: undefined` member on the signature, it now can resolve
63+
});
64+
}
65+
66+
//// [jsxLargeRefUnion.js]
67+
"use strict";
68+
/// <reference path="react16.d.ts" />
69+
var __assign = (this && this.__assign) || function () {
70+
__assign = Object.assign || function(t) {
71+
for (var s, i = 1, n = arguments.length; i < n; i++) {
72+
s = arguments[i];
73+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
74+
t[p] = s[p];
75+
}
76+
return t;
77+
};
78+
return __assign.apply(this, arguments);
79+
};
80+
var __rest = (this && this.__rest) || function (s, e) {
81+
var t = {};
82+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
83+
t[p] = s[p];
84+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
85+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
86+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
87+
t[p[i]] = s[p[i]];
88+
}
89+
return t;
90+
};
91+
exports.__esModule = true;
92+
exports.test2 = exports.test = void 0;
93+
var React = require("react");
94+
var animated = {};
95+
function makeAnimated(comp) {
96+
return null; // not important
97+
}
98+
function test(component) {
99+
// changing to `const Comp: any` un-hangs tsserver
100+
var Comp = typeof component === "string"
101+
? animated[component]
102+
: makeAnimated(component);
103+
return React.forwardRef(function (props, ref) {
104+
var show = props.show, ownProps = __rest(props, ["show"]);
105+
return show ? React.createElement(Comp, __assign({}, ownProps, { ref: ref })) : null; // ref as currently defined is expression-too-complex
106+
});
107+
}
108+
exports.test = test;
109+
function test2(component) {
110+
// changing to `const Comp: any` un-hangs tsserver
111+
var Comp = typeof component === "string"
112+
? animated[component]
113+
: makeAnimated(component);
114+
return forwardCustomRef(function (props, ref) {
115+
var show = props.show, ownProps = __rest(props, ["show"]);
116+
return show ? React.createElement(Comp, __assign({}, ownProps, { customRef: ref })) : null; // with the additional `current?: undefined` member on the signature, it now can resolve
117+
});
118+
}
119+
exports.test2 = test2;

0 commit comments

Comments
 (0)