Open Jym77 opened 1 year ago
If you were close to hitting the limit before and inference slightly changed in a generally non-observable way (say, to fix a bug), you might end up hitting the limit. Encountering the error is not a per se defect; we'd need a minimal example of this being a false positive to investigate further.
@RyanCavanaugh Thanks for the explanation. I'm trying to trim that code and see if I can extract a workable example.
Deep down, this code should be quite far from the limit; it is modelling CSS properties and the types of their values, which shouldn't be anywhere near 100,000 🤔 OTOH, there is a map of functions whose strict type gets lost and that could cause a multiplication of cardinals of unions when we retrieve stuff, leading to the explosion 💥 Hopefully, I'll manage to either get a workable example, or improve my code to keep the numbers low enough 😅
@RyanCavanaugh I've managed to extract a MNWE 🎉
Updated the original message with code and link to playground.
It is still 80 lines long, and the union should be around 50 items big. My attempts at simplifying it further cleared up the error (notably, the Keyword
class, which ends up being just a wrapper for string
, is still needed to confuse TS; and obviously removing some items from the unions goes below the limit).
Interestingly, my real code does build in TS 4.8.4, but this simplified version breaks all the way back to 4.1.5 (in 4.0.5, another error pops up preventing this one…) I'm a bit surprised by that given how much of a simplification I did on the way (notably shrinking down the union quite a lot…)
We've encountered this too when trying to migrate to from Typescript v4 to v5. Possible duplicate of #52459.
:wave: Hi, I'm the Repro bot. I can help narrow down and track compiler bugs across releases! This comment reflects the current state of the repro in the issue body running against the nightly TypeScript.
Issue body code block by @Jym77
:x: Failed: -
Expression produces a union type that is too complex to represent.
Version | Reproduction Outputs |
---|---|
4.6.2, 4.7.2, 4.8.2, 4.9.3, 5.0.2 |
:x: Failed: -
|
Investigating the minimal repro posted here, it doesn't seem like this is a bug. What happens is that, when we try to compute the type of property.compute
on line 6, we use the constraint type of the generic type of property. That type is Property<Family> | Property<Size> | ...
. Now, to get the signature of the compute
property from this union type of property
, we combine all of the .compute
signatures from the union components.
The way we combine the signatures is by intersecting the parameter types, and unioning the return types.
Now, the problem in the example is that intersecting the parameter types causes us to produce a type that is too complex to represent.
To see why, note that first we intersect types Family
and Size
, producing a simple intersection Family & Size
because Family
and Size
are plain object types. Now, when we go and intersect that with Stretch
, which is a union of 10 object types, we start getting a really big type, because to intersect type A
and type B | C | D
, we do a cross product and end up with A & B | A & C | A & D
. So now, intersecting Family & Size
and Stretch
gives us a union of 10 intersections. Then further, we intersect that result with type EastAsian
, which again is a union of 10 types, and we already had a union of 10 types, so now we end up with a union of 100 types. If you do the math, you can see that, by the time we try to intersect Weight
to this big param type we have so far, we already have a union of 27k types, and Weight
is a union of 5 types, so intersecting them would give us a union of 135k types, which is above the limit of 100k.
hi @gabritto
thanks for the explanation. Following the thread as we have a similar issue
do you know why this could have appeared with typescript 5 ? could it be related to the fact that enums are now "considered" unions ?
thanks!
hi @gabritto
thanks for the explanation. Following the thread as we have a similar issue
do you know why this could have appeared with typescript 5 ? could it be related to the fact that enums are now "considered" unions ?
thanks!
Honestly, it could be a number of different things, I can't tell without looking at an example.
Here's a simplified repro:
type Box<T> = { value: T };
type Func<T> = (x: T) => T;
declare const f1: Func<Box<10>> | Func<Box<11>>;
declare const f2:
Func<Box<10> | Box<11> | Box<12> | Box<13> | Box<14> | Box<15> | Box<16> | Box<17> | Box<18> | Box<19>> |
Func<Box<20> | Box<21> | Box<22> | Box<23> | Box<24> | Box<25> | Box<26> | Box<27> | Box<28> | Box<29>> |
Func<Box<30> | Box<31> | Box<32> | Box<33> | Box<34> | Box<35> | Box<36> | Box<37> | Box<38> | Box<39>> |
Func<Box<40> | Box<41> | Box<42> | Box<43> | Box<44> | Box<45> | Box<46> | Box<47> | Box<48> | Box<49>> |
Func<Box<50> | Box<51> | Box<52> | Box<53> | Box<54> | Box<55> | Box<56> | Box<57> | Box<58> | Box<59>>;
declare const x: Box<10>;
f1(x); // Error: Argument of type 'Box<10>' is not assignable to parameter of type 'never'.
f2(x); // Error: Expression produces a union type that is too complex to represent.
As f1
illustrates, it isn't possible to supply a parameter that simultaneously is both a Box<10>
and Box<11>
. Therefore, f1
reduces to a function that takes a never
parameter, i.e. a function that isn't callable.
The f2
declaration is just a more complicated way of writing a function that isn't callable. But, somewhat unrelated, the process of reducing the parameter type to never
causes construction of a very large intermediate union type that overflows our internal limit. But one way or the other, the types are never going to work out.
So, ultimately, I'd say this is working as intended.
I've been doing some digging, and it seems we've run into this problem before, of producing the "Expression produces a union type that is too complex to represent" for an intersection that would reduce to never
arising from combining signature parameter types: https://github.com/microsoft/TypeScript/issues/42790#issuecomment-781706671.
As mentioned in the linked comment, @weswigham even had a PR for it: https://github.com/microsoft/TypeScript/pull/42772.
It seems like this is something that comes up for people, given there are multiple issues mentioning this same problem and more people mentioning running into these issues. So maybe we could do something about the cases where the complex type would reduce to never
.
On the other hand, the error message is also very vague and not that helpful, it seems to me that people are surprised when their union of 50 or 200 elements becomes a union with more than 100k elements. But I don't really know what we could do about it. For instance, we could try to display the type somehow, but I'm not sure if displaying the type would help people in figuring out why we were trying to intersect a bunch of unions in the first place...
Interesting that we already have a PR in this area. We should bring it up to date and reevaluate it.
In an ideal world we'd quickly recognize intersections of object types that turn into never
(because one or more of their properties intersect to never
) and eliminate them early. Problem is, this requires eagerly resolving members and their types, which may lead to circularities. In @weswigham's PR we only attempt eager reduction when we're about to report an error anyways, and that may indeed be acceptable. But it sure would be nice to have a more robust scheme. I suspect it would require detecting properties with circular types and only deferring those, but I'm not sure how feasible that is.
@gabritto Thanks for looking into this.
So, if I get this correctly, this boils down to expanding cross products like (A | B) & (C | D)
into (A & C) | (A & D) | (B & C) | (B & D)
. I guess normalising as union of intersections makes sense most of the time, but sadly not in that one (where the intersection of unions is more compact).
I do agree that the error message was confusing, but I also fail to see how it could be made much better 🤔 In this case, this also plays on the subtlety of union of functions producing an intersection of paramters (due to contravariance).
Digging a bit more into it… It seems my MWNE is actually incorrect in what it tries to do.
function computed<N extends keyof Longhands>(
property: Longhands[N][1],
specified: Longhands[N][0]
) {
// error happens on this line
property.compute(specified);
}
Now, the intention is to only call the function with a property
that matches the specified
type. This is not the case here since I can call it with proprety: Property<Family>
and specified: Size
, which is incorrect (not in that example, but in my code base such a thing makes no sense).
This lack of link is (I understand) what makes the union of functions explode (the type of compute
is (Family -> Family) | (Size -> Size) | …
which is turned into (Family & Size & …) -> (Family | Size | …)
and the explosion happens at the left of the arrow).
Trying to enforce the link between both arguments:
declare const longhands: Longhands;
function computed2<N extends keyof Longhands>(name: N) {
const foo = longhands[name];
// error happens on this line
foo[1].compute(foo[0]);
}
I still get the same problem. But this time, it feels like foo[0]
and foo[1]
should be more closely tied (and notably, it is not possible to call that anymore with incompatible property
and specified
.
Yet, the error still occurs. My type theory is a bit rusty 😕 I'm likely missing some obvious reason why the type still need to be exploded here.
But I guess this leads to a question… Is there a way to type that function in a way that keeps the link between property
and specified
and therefore does not explode the union? Or a way to type foo
in the second version.
This sort of boils down to the fact that name
cannot be narrowed further than "font-family" | "font-size" | …
but in practice it is only going to be one of these…
function computed3(name: "font-family"): Family {
const foo = longhands[name];
return foo[1].compute(foo[0]);
}
works like a charm, but
function computed4<N extends "font-family" | "font-size">(name: N): Longhands[N][0] {
const foo = longhands[name];
// error happens on this line, foo[0] is not assignable to never
return foo[1].compute(foo[0]);
}
breaks because foo[0]
is of type Family | Size
but compute
ends up being of type F -> F | S -> S
, reduced to (F & S) -> (F | S)
, reduced to never -> (F | S)
.
Yet, in practice, name
can only be one of the two possibilities and this code does look type-safe 🤔
Am I missing something here? Is there a way to say that foo[0]
is always going to be of a type compatible for foo[1]
?
Trying to enforce the link between both arguments: I still get the same problem. But this time, it feels like foo[0] and foo[1] should be more closely tied (and notably, it is not possible to call that anymore with incompatible property and specified. Yet, the error still occurs. My type theory is a bit rusty 😕 I'm likely missing some obvious reason why the type still need to be exploded here.
You're right. The fact that we "explode" the type (i.e. we use a type parameter's constraint instead of the type parameter when resolving a type), and that we don't have a concept of "name
can only be one of the types in the union", are a design choice/limitation of TypeScript.
Typescript: 4.3.5: lodash.get funtion giving similar issue:
get(item, [recursiveKey, 'length'])
Bug Report
🔎 Search Terms
"Expression produces a union type that is too complex to represent" (found #33130 which seems unrelated as it has been fixed)
🕗 Version & Regression Information
⏯ Playground Link
Example in Playground
💻 Code
Note: interestingly, I detected the error in the much more complex real code of Alfa where it only triggers in TS 4.9.3, but not in 4.8.4 🤔 I am not sure which simplification happens in my real code to keep the union smaller…
🙁 Actual behavior
The code produces the error:
🙂 Expected behavior
The union should be around 50 items big, far below the limit.