Performance and usablity of `.exhaustive`
See original GitHub issueSo I noticed that exhaustive
has some pretty punishing compile times for any moderately complicated types it needs to match on. I’ve even found it hitting the “union type that is too complex to represent” limit regularly. I don’t have any benchmarks. but I think you are aware of the problem as you mention it your docs. I’m not sure if exhaustive
is actually usable except for the most simple cases.
If I had to guess it is because if you have a type like this:
type A = {type: "a", mode: "b" | "c" | "d"} | {type: "b", mode: "f" | "g"}
You have to generate a union that looks like this?:
| {type: "a", mode: "b"}
| {type: "a", mode: "c"}
| {type: "a", mode: "d"}
| {type: "b", mode: "f"}
| {type: "b", mode: "g"}
So for example this fairly simple to understand union will completely break exhaustive
:
import { Property } from "csstype";
declare const a:
| { type: "textWithColor"; color: Property.Color }
| { type: "textWithColorAndBackground"; color: Property.Color; backgroundColor: Property.Color };
match2(a).exhaustive(); // "union type that is too complex to represent"
playground link - takes serveral minutes on my machine to hit the limit
The reason being is that Property.Color
is a string union with hundreds of variants. (this is the same kind of example that eventually lead the typescript team to abandon by default inference of template string literals types in 4.2 - https://github.com/microsoft/TypeScript/issues/42416)
So this makes exhaustive
pretty much unusable if you have a type with properties that are unions of any moderate size. Either because you will hit the union limit or because the compile times are too extreme to make it practical to use.
This is all fair, and I don’t think I see a way around the issue and keeping the full pattern matching features of the lib.
That said, in 80-90% of cases all I want to match on is the discriminator of a union in order to narrow the types. For example this works in a simple switch and I still get some kind of exhaustiveness checks:
declare const a:
| { type: "textWithColor"; color: Property.Color }
| { type: "textWithColorAndBackground"; color: Property.Color; backgroundColor: Property.Color };
const aToDescription (a: typeof a): string => {
switch(a.type) {
case "textWithColor":
return a.color
case "textWithColorAndBackground":
return `${a.color} with a background of ${a.backgroundColor}`
}
}
I’m wondering if you can offer something that still allows for some kind of exhaustiveness checks on discriminators in order to narrow types down, but compromises on pattern matching features elsewhere for the sake of compile time performance. In the back of my mind I’m thinking about this lib https://paarthenon.github.io/variant/ (mentioned in my my previous issue) because this library can do exhaustiveness checks because it only concerns itself with the discriminators of unions.
Issue Analytics
- State:
- Created 3 years ago
- Comments:16 (16 by maintainers)
Just gave it a quick whirl. It feels great - performance is significantly better. I threw a few large and complex types at it, and I didn’t run into any issues. The error messages you are able to get on
run
are wonderful. I think you managed to do it - you have made the union explosion an opt in that a user can mitigate by say nesting a match within a match.Having a usable
exhaustive
makes me much more likely to start using this library in lots of places, and get buy in by my team.👋 I think I found a pretty good implementation of the
DeepExclude<A, B>
I described above that doesn’t distribute every unions upfront. It seems to work pretty well and performances seem acceptable. See PR #17I just released the
ts-pattern@2.1.3-next.0
pre-release with the updated types forexhaustive()
, do you mind installing it on your project and telling me if it solves the performance problem you were facing or at least if it’s better?You can also play with it in this sandbox: https://codesandbox.io/s/autumn-https-41um5?file=/src/index.ts