Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expression produces a union type that is too complex to represent #53234

Open
Jym77 opened this issue Mar 13, 2023 · 17 comments · May be fixed by #53739
Open

Expression produces a union type that is too complex to represent #53234

Jym77 opened this issue Mar 13, 2023 · 17 comments · May be fixed by #53739
Assignees
Labels
Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros Needs Investigation This issue needs a team member to investigate its status.

Comments

@Jym77
Copy link

Jym77 commented Mar 13, 2023

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

  • Present from 4.1.5 to 5.0.2 in playground.

⏯ Playground Link

Example in Playground

💻 Code

function computed<N extends keyof Longhands>(
  property: Longhands[N][1],
  specified: Longhands[N][0]
) {
  // error happens on this line
  property.compute(specified);
}

interface Property<T> {
  compute: (value: T) => T;
}

type Wrapper<T> = [T, Property<T>];

interface Longhands {
  "font-family": Wrapper<Family>;
  "font-size": Wrapper<Size>;
  "font-stretch": Wrapper<Stretch>;
  "font-variant-caps": Wrapper<Caps>;
  "font-variant-east-asian": Wrapper<EastAsian>;
  "font-variant-ligatures": Wrapper<Ligatures>;
  "font-variant-numeric": Wrapper<Numeric>;
  "font-variant-position": Wrapper<Position>;
  "font-weight": Wrapper<Weight>;
}

class Keyword<K extends string> {
  keyword: K;
  constructor(keyword: K) {
    this.keyword = keyword;
  }
}

type Family = Keyword<"serif">;
type Size = Keyword<"length">;
type Stretch =
  | 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">;
type Caps = Keyword<"normal">;
type EastAsian =
  | Keyword<"normal">
  | Keyword<"jis78">
  | Keyword<"jis83">
  | Keyword<"jis90">
  | Keyword<"jis04">
  | Keyword<"simplified">
  | Keyword<"traditional">
  | Keyword<"proportional-width">
  | Keyword<"full-width">
  | Keyword<"ruby">;
type Ligatures =
  | Keyword<"none">
  | Keyword<"normal">
  | 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">;
type Numeric =
  | Keyword<"normal">
  | Keyword<"lining-nums">
  | Keyword<"oldstyle-nums">
  | Keyword<"proportional-nums">
  | Keyword<"tabular-nums">
  | Keyword<"diagonal-fractions">
  | Keyword<"stacked-fractions">
  | Keyword<"ordinal">
  | Keyword<"slashed-zero">;
type Position = Keyword<"normal"> | Keyword<"sub"> | Keyword<"super">;
type Weight =
  | Keyword<"number">
  | Keyword<"normal">
  | Keyword<"bold">
  | Keyword<"bolder">
  | Keyword<"lighter">;

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:

Expression produces a union type that is too complex to represent

🙂 Expected behavior

The union should be around 50 items big, far below the limit.

@RyanCavanaugh
Copy link
Member

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 RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Mar 14, 2023
@Jym77
Copy link
Author

Jym77 commented Mar 16, 2023

@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 😅

@Jym77
Copy link
Author

Jym77 commented Mar 20, 2023

@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…)

@nick-kang
Copy link

We've encountered this too when trying to migrate to from Typescript v4 to v5. Possible duplicate of #52459.

@RyanCavanaugh RyanCavanaugh removed the Not a Defect This behavior is one of several equally-correct options label Mar 30, 2023
@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Mar 30, 2023
@typescript-bot typescript-bot added the Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros label Mar 30, 2023
@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 31, 2023

👋 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

❌ Failed: -

  • Expression produces a union type that is too complex to represent.

Historical Information
Version Reproduction Outputs
4.6.2, 4.7.2, 4.8.2, 4.9.3, 5.0.2

❌ Failed: -

  • Expression produces a union type that is too complex to represent.

@gabritto
Copy link
Member

gabritto commented Apr 5, 2023

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.

@eltonio450
Copy link

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!

@gabritto
Copy link
Member

gabritto commented Apr 5, 2023

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.

@ahejlsberg
Copy link
Member

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.

@ahejlsberg
Copy link
Member

So, ultimately, I'd say this is working as intended.

@gabritto
Copy link
Member

gabritto commented Apr 6, 2023

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: #42790 (comment).
As mentioned in the linked comment, @weswigham even had a PR for it: #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.

@gabritto
Copy link
Member

gabritto commented Apr 6, 2023

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...

@ahejlsberg
Copy link
Member

ahejlsberg commented Apr 6, 2023

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.

@Jym77
Copy link
Author

Jym77 commented Apr 12, 2023

@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).

@Jym77
Copy link
Author

Jym77 commented Apr 28, 2023

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]?

@gabritto
Copy link
Member

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.

@prashantbiradarvrts
Copy link

Typescript: 4.3.5: lodash.get funtion giving similar issue:

get(item, [recursiveKey, 'length']) 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants