microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.31k stars 12.53k forks source link

an emitter with 239 events breaks type check, throw ts2590 Expression produces a union type that is too complex to represent #42790

Open shigma opened 3 years ago

shigma commented 3 years ago

Bug Report

I wrote a framework (namely koishi) which uses a fully-typed event emitter implemented by itself. However when I create more events (totally 239), the type check just broke with an error ts2590 (Expression produces a union type that is too complex to represent). It seems that the error was thrown here:

image

I know 239 events is a lot, but I still cannot figure out how the 239 events became "100000 type checks".

Can you help prevent this error either by optimize type check performance or by improve my code? Thanks.

🔎 Search Terms

union type, interface, event emitter, ts2590

🕗 Version & Regression Information

I tried with v4.1.3, v4.1.5 & v4.2.1-rc. They all failed.

⏯ Playground Link

This playground will not work because it imports a dependency @octokit/webhooks-definitions (it's just a simple collection of github webhooks type definitions, but it is too big (over 5000 lines) to be included as I thought).

Playground link with a dependency

And the dependency code can be found here: https://unpkg.com/@octokit/webhooks-definitions@3.60.0/schema.d.ts

💻 Code

interface EventMap extends SessionEventMap, WebhookEventMap {}

declare function on<K extends keyof EventMap>(name: K, listener: EventMap[K]): () => boolean

on('message', () => {})

🙁 Actual behavior

TS2590 error: Expression produces a union type that is too complex to represent.

🙂 Expected behavior

No compiler error

RyanCavanaugh commented 3 years ago

@ahejlsberg or @weswigham interested in looking at this?

weswigham commented 3 years ago

I have looked at this and have a pretty good grasp of what's up at this point. First: You "only" have 239 top-level events... but each of those events may have multiple possible payload shapes. So the handler arrow function signature's contextual type looks like (payload: A1) => void | ... | (payload: A239) => void, where A1 through A239 are themselves unions of 1 or more object types. When we have a contextual union type like this, we attempt to combine those disparate signatures into a single contextual signature. To do that, we intersect all those parameter types. That's where the limiter comes into play - our simple analysis sees that based on the union+nested intersection count, there's >200,000 possible combinations in that calculated parameter type intersection. What that complexity analysis doesn't currently know, however, is that almost all of those 200,000+ possible combinations will reduce away to never due to conflicting discriminant properties (their type field).

So there's two things I've come up with that we can do to improve here, and I have implementations for both, I just don't know if we wanna do both, and, if so, together or separately.

  1. In the example given, the callback passed to on has no explicit arguments. That means we shouldn't need to actually look at the argument types of the composite signature (since there's no argument position we actually need to contextually type). We can make it so we don't eagerly pull on and normalize the constituent types with a pretty straightforward usage of and modification to our existing SymbolFlags.DeferredType machinery for deferring union/intersection property type normalization (so it also covers union/intersection parameters). That will help the example as written, but if you rewrite it so it actually has an argument, eg, on('message', arg => {}), you're right back to an expression complexity error. Still, that allows the example given to compile in ~2s in my machine without a complexity error.
  2. Given that, integrating some kind of eager reduction pass seems useful in scenarios like this (where we're probably going to hit a complexity limit if we don't simplify the types involved). I already take a similar approach in https://github.com/microsoft/TypeScript/pull/42772 - with some slight modifications, the scenario given here can also be caught by that PR's check and eagerly reduced. Doing so can remove the complexity error - however performing the reduction pass isn't free. We only need to do it once in the whole program, no matter how many times on is called (because it's part of a constraint calculation on the signature of on), so the overhead is relatively fixed... but actually reducing the type just that one time adds 52s to what is otherwise a 2s compilation on my really beefy machine. This might be able to be reduced with some investigation in faster recognition/application of reduction. Anders' recent work may be relevant here.
alesmenzel commented 2 years ago

Hello, are there any plans to handle this particular issue? I am still seeing this error on typescript 4.5.5. Or Can you please provide a workaround like a tsconfig.json settings to disable/set higher limit for the too complex type error?