-
-
Notifications
You must be signed in to change notification settings - Fork 140
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
Performance and usablity of .exhaustive
#16
Comments
I do like the library - the pattern matching outside of exhaustive seems pretty cool. I quite like |
Hey!
That's exactly right.
It looks like the constrains are a bit different between ts-pattern and variant (a lib I didn't know before to be honest). If I'm not mistaken, variant wants to control the way you create your union type, which means that you always end up with a flat union of objects. ts-pattern however wants to work with any data-structure and support recursive patterns, that's why it's computing all possible cases of the input type upfront. That said, I'm aware the current state of exhaustive matching isn't ideal and I agree on the fact that we should find a better balance between 1. letting you exclude cases based on a deep patterns, matching several unions at once and 2. support input containing huge union types, like Here are the possible options I can think of: 1. When traversing the input type, if we encounter a union of objects, stop the recursion here and don't try to find unions contained in each case. The down side is that cases like: type Input = { kind: 'none' } | { kind: 'some', value: number | string }
match<Input>(input)
.exhaustive()
.with({ kind: 'none' }, () => out)
.with({ kind: 'some', value: __.number }, () => out)
.with({ kind: 'some', value: __.string }, () => out)
.run() wouldn't work anymore. 2. have some kind of heuristic to avoid including unions that exceed 3. Instead of computing all cases upfront when calling Do you see any other solution I might be missing? |
Yes, I've realised now that the two libraries are similar in terms of the problem they are trying to solve and their means of solving it, but are quite different and maybe even complement each other to a certain extent. The
I'm not sure if I have another option to add, but I think it might be wise to make the disclaimer around |
Another options: match(a)exhaustive("type") // exhaustively match on this property only? |
match(a)exhaustive("type", "mode") // exhaustively match on these two properties? |
The idea is essentially you give the user control over how exhaustive the pattern matching checks are by forcing them to opt into the relevant fields. Downside is you probably cannot allow matching on properties outside of that list. |
Interesting idea, but how would this look like for non-object patterns (tuples for instance), and what if you want to have exhaustive checking on properties at different level of nesting? let's say you have an input like this for instance: type Option<T> = { kind: "none" } | { kind: "some", value: T };
type Input = [Option<{ type: 'a' } | { type: 'b' }>, 'c' | 'd']
match<Input>(input)
.exhaustive(?) this can sound a bit theoretical, but this is the kind of usecase this lib tries to handle well |
Btw I've started thinking about option 3, and I think it might be possible to avoid distributing all the possible cases contained in the input upfront by writing a type Input =
| { type: "textWithColor"; color: Property.Color }
| { type: "textWithColorAndBackground"; color: Property.Color; backgroundColor: Property.Color };
type x1 = DeepExclude<Input, { type: "textWithColor" }>
// => { type: "textWithColorAndBackground"; color: Property.Color; backgroundColor: Property.Color };
type x2 = DeepExclude<Input, { type: "textWithColor", color: "grey" }>
// => | { type: "textWithColorAndBackground"; color: Property.Color; backgroundColor: Property.Color }
// | { type: "textWithColor", color: "pink" }
// | { type: "textWithColor", color: "purple" }
// | { type: "textWithColor", color: "red" }
// | ... all possible colors, because we need to distribute at this point, but not that the `type: "textWithColorAndBackground"` case hasn't been distributed The first argument is the input, and the second argument is the type of the pattern given to I'll try to do that this weekend, but it will probably be hard, and the perf might not be good enough. Also it took me ~ 2 months to find a way to implement the current version of the exhaustive matching, so it might take a while :) |
Oh that is interesting. If that is possible, then it at least makes it less likely that the match will hit or go near the union limit. I guess it still means that if a user does match on |
👋 I think I found a pretty good implementation of the I just released the You can also play with it in this sandbox: https://codesandbox.io/s/autumn-https-41um5?file=/src/index.ts |
I'll be sure to check out the pre-release! |
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 Having a usable |
I suggest you document why something like this is problematic: const output = match<Input>(input)
.exhaustive() // it works and it's fast!
.with({ type: "text", color: "grey" }, () => `<p>some text</p>`)
.with({ type: "button", color: "grey" }, () => `<button>click</button>`)
.with({ type: "button", bgColor: "grey" }, () => `<button>click</button>`)
.run();
// Type instantiation is excessively deep and possibly infinite.ts(2589) |
possible mitigation example: const output = match<Input>(input)
.exhaustive() // it works and it's fast!
.with({ type: "text" }, () => `<p>some text</p>`)
.with({ type: "button"}, (input) =>
match(input)
.with({color: "grey"}, () => "<button> grey button click</button>" )
.otherwise(() => "<button>click</button>" )
)
.run();
You are exhaustive on the things you care about, and handle more narrow patterns within a separate match on the narrowed type which itself may or may not be exhaustive |
Good call, I'll add some documentation about this once I ship the new version |
Closing this issue as the |
So 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 ifexhaustive
is actually usable except for the most simple cases.If I had to guess it is because if you have a type like this:
You have to generate a union that looks like this?:
So for example this fairly simple to understand union will completely break
exhaustive
: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 - microsoft/TypeScript#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:
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.
The text was updated successfully, but these errors were encountered: