Skip to content

Commit 757c56a

Browse files
committed
Reduce at more stages, add test from related issue
1 parent 2d21364 commit 757c56a

6 files changed

+736
-30
lines changed

src/compiler/checker.ts

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16827,36 +16827,39 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1682716827
result = getUnionType([getIntersectionType(typeSet), nullType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments);
1682816828
}
1682916829
else {
16830-
if (typeSet.length > 2 && getCrossProductUnionSize(typeSet) >= UNION_CROSS_PRODUCT_SIZE_LIMIT && every(typeSet, t => !!(t.flags & TypeFlags.Union) || !!(t.flags & TypeFlags.Primitive))) {
16831-
// This type set is going to trigger an "expression too complex" error below. Rather than resort to that, as a last, best effort, simplify the type.
16832-
// When the intersection looks like (A | B | C) & (D | E | F) & (G | H | I) - in the general case, this can result in a massive resulting
16833-
// union, hence the check on the cross product size below, _however_ in some cases we can also _simplify_ the resulting type massively.
16834-
// If we can recognize that upfront, we can still allow the type to form without creating innumerable intermediate types.
16835-
// Specifically, in cases where almost all combinations are known to reduce to `never` (so the result is essentially sparse)
16836-
// and we can recognize that quickly, we can use a simplified result without checking the worst-case size.
16837-
// So we start with the assumption that the result _is_ sparse when the input looks like the above, and we assume the result
16838-
// will take the form (A & D & G) | (B & E & H) | (C & F & I). To validate this, we reduce left, first combining
16839-
// (A | B | C) & (D | E | F); if that combines into `(A & D) | (B & E) | (C & F)` like we want, which we make 9 intermediate
16840-
// 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
16841-
// to check, finally producing `(A & D & G) | (B & E & H) | (C & F & I)`. This required 18 intermediate types, while the standard method
16842-
// of expanding (A | B | C) & (D | E | F) & (G | H | I) would produce 27 types and then perform reduction on the result.
16843-
// By going elemnt-wise, and bailing if the result fails to reduce, we can allow these sparse expansions without doing undue work.
16844-
runningResult = typeSet[0];
16845-
for (let i = 1; i < typeSet.length; i++) {
16846-
// For intersection reduction, here we're considering `undefined & (A | B)` as `never`. (ie, we're disallowing branded primitives)
16847-
// This is relevant for, eg, when looking at `(HTMLElement | null) & (SVGElement | null) & ... & undefined` where _usually_
16848-
// we'd allow for tons of garbage intermediate types like `null & SVGElement` to exist; but nobody ever really actually _wants_
16849-
// that, IMO. Those types can still exist in the type system; just... not when working with unions and intersections with massive
16850-
// cross-product growth potential.
16851-
runningResult = typeSet[i].flags & TypeFlags.Primitive && everyType(runningResult, t => !!(t.flags & TypeFlags.Object)) ? neverType : getReducedType(intersectTypes(runningResult, typeSet[i]));
16852-
if (i === typeSet.length - 1 || isTypeAny(runningResult) || runningResult.flags & TypeFlags.Never) {
16853-
return runningResult;
16854-
}
16855-
if (!(runningResult.flags & TypeFlags.Union) || (runningResult as UnionType).types.length > typeSet.length) {
16856-
// Save work done by the accumulated result thus far, even if we're bailing on the heuristic.
16857-
// It may have saved us enough work already that we're willing to work with the type now.
16858-
typeSet = typeSet.slice(i + 1);
16859-
break;
16830+
if (getCrossProductUnionSize(typeSet) >= UNION_CROSS_PRODUCT_SIZE_LIMIT && every(typeSet, t => !!(t.flags & TypeFlags.Union) || !!(t.flags & TypeFlags.Primitive))) {
16831+
if (typeSet.length > 2 || some(typeSet, t => getReducedType(t) !== t)) {
16832+
// This type set is going to trigger an "expression too complex" error below. Rather than resort to that, as a last, best effort, simplify the type.
16833+
// When the intersection looks like (A | B | C) & (D | E | F) & (G | H | I) - in the general case, this can result in a massive resulting
16834+
// union, hence the check on the cross product size below, _however_ in some cases we can also _simplify_ the resulting type massively.
16835+
// If we can recognize that upfront, we can still allow the type to form without creating innumerable intermediate types.
16836+
// Specifically, in cases where almost all combinations are known to reduce to `never` (so the result is essentially sparse)
16837+
// and we can recognize that quickly, we can use a simplified result without checking the worst-case size.
16838+
// So we start with the assumption that the result _is_ sparse when the input looks like the above, and we assume the result
16839+
// will take the form (A & D & G) | (B & E & H) | (C & F & I). To validate this, we reduce left, first combining
16840+
// (A | B | C) & (D | E | F); if that combines into `(A & D) | (B & E) | (C & F)` like we want, which we make 9 intermediate
16841+
// 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
16842+
// to check, finally producing `(A & D & G) | (B & E & H) | (C & F & I)`. This required 18 intermediate types, while the standard method
16843+
// of expanding (A | B | C) & (D | E | F) & (G | H | I) would produce 27 types and then perform reduction on the result.
16844+
// By going elemnt-wise, and bailing if the result fails to reduce, we can allow these sparse expansions without doing undue work.
16845+
runningResult = getReducedType(typeSet[0]);
16846+
for (let i = 1; i < typeSet.length; i++) {
16847+
// For intersection reduction, here we're considering `undefined & (A | B)` as `never`. (ie, we're disallowing branded primitives)
16848+
// This is relevant for, eg, when looking at `(HTMLElement | null) & (SVGElement | null) & ... & undefined` where _usually_
16849+
// we'd allow for tons of garbage intermediate types like `null & SVGElement` to exist; but nobody ever really actually _wants_
16850+
// that, IMO. Those types can still exist in the type system; just... not when working with unions and intersections with massive
16851+
// cross-product growth potential.
16852+
const reducedElem = getReducedType(typeSet[i]);
16853+
runningResult = reducedElem.flags & TypeFlags.Primitive && everyType(runningResult, t => !!(t.flags & TypeFlags.Object)) ? neverType : getReducedType(intersectTypes(runningResult, reducedElem));
16854+
if (i === typeSet.length - 1 || isTypeAny(runningResult) || runningResult.flags & TypeFlags.Never) {
16855+
return runningResult;
16856+
}
16857+
if (!(runningResult.flags & TypeFlags.Union) || (runningResult as UnionType).types.length > typeSet.length) {
16858+
// Save work done by the accumulated result thus far, even if we're bailing on the heuristic.
16859+
// It may have saved us enough work already that we're willing to work with the type now.
16860+
typeSet = typeSet.slice(i + 1);
16861+
break;
16862+
}
1686016863
}
1686116864
}
1686216865
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
tests/cases/compiler/simplifyingExpressionTypeNotTooComplexToRepresent.ts(6,22): error TS2345: Argument of type 'Family | Size | Keyword<"percentage"> | Keyword<"ultra-condensed"> | Keyword<"extra-condensed"> | Keyword<"condensed"> | Keyword<"semi-condensed"> | Keyword<"normal"> | Keyword<"semi-expanded"> | Keyword<"expanded"> | Keyword<"extra-expanded"> | Keyword<"ultra-expanded"> | Caps | Keyword<"jis78"> | Keyword<"jis83"> | Keyword<"jis90"> | Keyword<"jis04"> | Keyword<"simplified"> | Keyword<"traditional"> | Keyword<"proportional-width"> | Keyword<"full-width"> | Keyword<"ruby"> | Keyword<"none"> | Keyword<"common-ligatures"> | Keyword<"no-common-ligatures"> | Keyword<"discretionary-ligatures"> | Keyword<"no-discretionary-ligatures"> | Keyword<"historical-ligatures"> | Keyword<"no-historical-ligatures"> | Keyword<"contextual"> | Keyword<"no-contextual"> | Keyword<"lining-nums"> | Keyword<"oldstyle-nums"> | Keyword<"proportional-nums"> | Keyword<"tabular-nums"> | Keyword<"diagonal-fractions"> | Keyword<"stacked-fractions"> | Keyword<"ordinal"> | Keyword<"slashed-zero"> | Keyword<"sub"> | Keyword<"super"> | Keyword<"number"> | Keyword<"bold"> | Keyword<"bolder"> | Keyword<"lighter">' is not assignable to parameter of type 'never'.
2+
Type 'Family' is not assignable to type 'never'.
3+
4+
5+
==== tests/cases/compiler/simplifyingExpressionTypeNotTooComplexToRepresent.ts (1 errors) ====
6+
function computed<N extends keyof Longhands>(
7+
property: Longhands[N][1],
8+
specified: Longhands[N][0]
9+
) {
10+
// error happens on this line
11+
property.compute(specified);
12+
~~~~~~~~~
13+
!!! error TS2345: Argument of type 'Family | Size | Keyword<"percentage"> | Keyword<"ultra-condensed"> | Keyword<"extra-condensed"> | Keyword<"condensed"> | Keyword<"semi-condensed"> | Keyword<"normal"> | Keyword<"semi-expanded"> | Keyword<"expanded"> | Keyword<"extra-expanded"> | Keyword<"ultra-expanded"> | Caps | Keyword<"jis78"> | Keyword<"jis83"> | Keyword<"jis90"> | Keyword<"jis04"> | Keyword<"simplified"> | Keyword<"traditional"> | Keyword<"proportional-width"> | Keyword<"full-width"> | Keyword<"ruby"> | Keyword<"none"> | Keyword<"common-ligatures"> | Keyword<"no-common-ligatures"> | Keyword<"discretionary-ligatures"> | Keyword<"no-discretionary-ligatures"> | Keyword<"historical-ligatures"> | Keyword<"no-historical-ligatures"> | Keyword<"contextual"> | Keyword<"no-contextual"> | Keyword<"lining-nums"> | Keyword<"oldstyle-nums"> | Keyword<"proportional-nums"> | Keyword<"tabular-nums"> | Keyword<"diagonal-fractions"> | Keyword<"stacked-fractions"> | Keyword<"ordinal"> | Keyword<"slashed-zero"> | Keyword<"sub"> | Keyword<"super"> | Keyword<"number"> | Keyword<"bold"> | Keyword<"bolder"> | Keyword<"lighter">' is not assignable to parameter of type 'never'.
14+
!!! error TS2345: Type 'Family' is not assignable to type 'never'.
15+
}
16+
17+
interface Property<T> {
18+
compute: (value: T) => T;
19+
}
20+
21+
type Wrapper<T> = [T, Property<T>];
22+
23+
interface Longhands {
24+
"font-family": Wrapper<Family>;
25+
"font-size": Wrapper<Size>;
26+
"font-stretch": Wrapper<Stretch>;
27+
"font-variant-caps": Wrapper<Caps>;
28+
"font-variant-east-asian": Wrapper<EastAsian>;
29+
"font-variant-ligatures": Wrapper<Ligatures>;
30+
"font-variant-numeric": Wrapper<Numeric>;
31+
"font-variant-position": Wrapper<Position>;
32+
"font-weight": Wrapper<Weight>;
33+
}
34+
35+
class Keyword<K extends string> {
36+
keyword: K;
37+
constructor(keyword: K) {
38+
this.keyword = keyword;
39+
}
40+
}
41+
42+
type Family = Keyword<"serif">;
43+
type Size = Keyword<"length">;
44+
type Stretch =
45+
| Keyword<"percentage">
46+
| Keyword<"ultra-condensed">
47+
| Keyword<"extra-condensed">
48+
| Keyword<"condensed">
49+
| Keyword<"semi-condensed">
50+
| Keyword<"normal">
51+
| Keyword<"semi-expanded">
52+
| Keyword<"expanded">
53+
| Keyword<"extra-expanded">
54+
| Keyword<"ultra-expanded">;
55+
type Caps = Keyword<"normal">;
56+
type EastAsian =
57+
| Keyword<"normal">
58+
| Keyword<"jis78">
59+
| Keyword<"jis83">
60+
| Keyword<"jis90">
61+
| Keyword<"jis04">
62+
| Keyword<"simplified">
63+
| Keyword<"traditional">
64+
| Keyword<"proportional-width">
65+
| Keyword<"full-width">
66+
| Keyword<"ruby">;
67+
type Ligatures =
68+
| Keyword<"none">
69+
| Keyword<"normal">
70+
| Keyword<"common-ligatures">
71+
| Keyword<"no-common-ligatures">
72+
| Keyword<"discretionary-ligatures">
73+
| Keyword<"no-discretionary-ligatures">
74+
| Keyword<"historical-ligatures">
75+
| Keyword<"no-historical-ligatures">
76+
| Keyword<"contextual">
77+
| Keyword<"no-contextual">;
78+
type Numeric =
79+
| Keyword<"normal">
80+
| Keyword<"lining-nums">
81+
| Keyword<"oldstyle-nums">
82+
| Keyword<"proportional-nums">
83+
| Keyword<"tabular-nums">
84+
| Keyword<"diagonal-fractions">
85+
| Keyword<"stacked-fractions">
86+
| Keyword<"ordinal">
87+
| Keyword<"slashed-zero">;
88+
type Position = Keyword<"normal"> | Keyword<"sub"> | Keyword<"super">;
89+
type Weight =
90+
| Keyword<"number">
91+
| Keyword<"normal">
92+
| Keyword<"bold">
93+
| Keyword<"bolder">
94+
| Keyword<"lighter">;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//// [simplifyingExpressionTypeNotTooComplexToRepresent.ts]
2+
function computed<N extends keyof Longhands>(
3+
property: Longhands[N][1],
4+
specified: Longhands[N][0]
5+
) {
6+
// error happens on this line
7+
property.compute(specified);
8+
}
9+
10+
interface Property<T> {
11+
compute: (value: T) => T;
12+
}
13+
14+
type Wrapper<T> = [T, Property<T>];
15+
16+
interface Longhands {
17+
"font-family": Wrapper<Family>;
18+
"font-size": Wrapper<Size>;
19+
"font-stretch": Wrapper<Stretch>;
20+
"font-variant-caps": Wrapper<Caps>;
21+
"font-variant-east-asian": Wrapper<EastAsian>;
22+
"font-variant-ligatures": Wrapper<Ligatures>;
23+
"font-variant-numeric": Wrapper<Numeric>;
24+
"font-variant-position": Wrapper<Position>;
25+
"font-weight": Wrapper<Weight>;
26+
}
27+
28+
class Keyword<K extends string> {
29+
keyword: K;
30+
constructor(keyword: K) {
31+
this.keyword = keyword;
32+
}
33+
}
34+
35+
type Family = Keyword<"serif">;
36+
type Size = Keyword<"length">;
37+
type Stretch =
38+
| Keyword<"percentage">
39+
| Keyword<"ultra-condensed">
40+
| Keyword<"extra-condensed">
41+
| Keyword<"condensed">
42+
| Keyword<"semi-condensed">
43+
| Keyword<"normal">
44+
| Keyword<"semi-expanded">
45+
| Keyword<"expanded">
46+
| Keyword<"extra-expanded">
47+
| Keyword<"ultra-expanded">;
48+
type Caps = Keyword<"normal">;
49+
type EastAsian =
50+
| Keyword<"normal">
51+
| Keyword<"jis78">
52+
| Keyword<"jis83">
53+
| Keyword<"jis90">
54+
| Keyword<"jis04">
55+
| Keyword<"simplified">
56+
| Keyword<"traditional">
57+
| Keyword<"proportional-width">
58+
| Keyword<"full-width">
59+
| Keyword<"ruby">;
60+
type Ligatures =
61+
| Keyword<"none">
62+
| Keyword<"normal">
63+
| Keyword<"common-ligatures">
64+
| Keyword<"no-common-ligatures">
65+
| Keyword<"discretionary-ligatures">
66+
| Keyword<"no-discretionary-ligatures">
67+
| Keyword<"historical-ligatures">
68+
| Keyword<"no-historical-ligatures">
69+
| Keyword<"contextual">
70+
| Keyword<"no-contextual">;
71+
type Numeric =
72+
| Keyword<"normal">
73+
| Keyword<"lining-nums">
74+
| Keyword<"oldstyle-nums">
75+
| Keyword<"proportional-nums">
76+
| Keyword<"tabular-nums">
77+
| Keyword<"diagonal-fractions">
78+
| Keyword<"stacked-fractions">
79+
| Keyword<"ordinal">
80+
| Keyword<"slashed-zero">;
81+
type Position = Keyword<"normal"> | Keyword<"sub"> | Keyword<"super">;
82+
type Weight =
83+
| Keyword<"number">
84+
| Keyword<"normal">
85+
| Keyword<"bold">
86+
| Keyword<"bolder">
87+
| Keyword<"lighter">;
88+
89+
//// [simplifyingExpressionTypeNotTooComplexToRepresent.js]
90+
function computed(property, specified) {
91+
// error happens on this line
92+
property.compute(specified);
93+
}
94+
var Keyword = /** @class */ (function () {
95+
function Keyword(keyword) {
96+
this.keyword = keyword;
97+
}
98+
return Keyword;
99+
}());

0 commit comments

Comments
 (0)