gvergnaud / ts-pattern

🎨 The exhaustive Pattern Matching library for TypeScript, with smart type inference.
MIT License
12.44k stars 132 forks source link

Features to make libraries extend/play nice with ts-pattern #166

Open WoLfulus opened 1 year ago

WoLfulus commented 1 year ago

Is your feature request related to a problem? Please describe.

I found myself having to do a bunch of digging and workarounds to be able to "extend" ts-pattern to make my library able to play nice with it. I'm doing something similar to rust error handling, and I wanted to support cases like this:

class Response {}
class SuccessResponse extends Response {}
class JsonResponse extends SuccessResponse {}
class TextResponse extends SuccessResponse {}
class ErrorResponse extends Response {}
class AuthenticationError extends ErrorResponse {}

// ...

const res = await request('...');

const handled = match(res)
  .with(Ok(JsonResponse), handleJson)
  .with(Ok(TextResponse), handleText)
  .with(Ok(SuccessResponse), handleUnknownSuccess)
  .with(Ok(P.any), handleUnknownSuccessType)
  .with(Ok(), handleEmptySuccess)
  .with(Err(AuthenticationError), handleSuccess)
  .with(Err(ErrorResponse), handleUnknownError)
  .with(Err(P.any), handleUnknownErrorType)
  .with(Err(), handleEmptyErr)
  .otherwise(() => false);

I did manage to make this "work", but with a lot of workarounds.

Describe the solution you'd like

Describe alternatives you've considered

I'm sure there might be an easier way to do this, but this is what I came up with:

https://codesandbox.io/p/sandbox/extend-ts-pattern-nly6vh

I did a bunch of workarounds to get to what I expected the usage to be:

https://codesandbox.io/p/sandbox/extend-ts-pattern-nly6vh?file=%2Fsrc%2Findex.test.ts%3A32%2C1-53%2C1

I'm still unable to create my own custom guards that lets the user select a different subset of information easily:

https://codesandbox.io/p/sandbox/extend-ts-pattern-nly6vh?file=%2Fsrc%2Findex.test.ts%3A75%2C1-82%2C1

Even though it works close to what I expect, it will all fall apart if you strong type "res" to anything other than any when calling match() (for example res: Response, which is the base class)

WoLfulus commented 1 year ago

Thanks for v5! It addresses some of the isseus I had, but ending with multiple instances of matcher symbol is still a huge problem. I couldn't debug enough to see where the issue comes from, but I'm using next.js with ts-pattern and my librart as a dependency (mine has ts-pattern as a peer dependency). They both targets the same ts-pattern version and pnpm reports only 1 installation, so I think it's a bundler problem in this case.

I'm unable to create a pattern inside my package and use it inside the next.js project because ts-pattern won't be able to check for the [matcher] symbol, and I'd like to avoid having to inject ts-pattern instance into my package, since it completely breaks DX I'm hoping for :(

Anyways, I think turning matcher into a Symbol.for() would help in this specific case.

gvergnaud commented 1 year ago

Thanks for opening this issue @WoLfulus

I didn't realize there would be a problem when using two concurrent versions of ts-pattern in the same codebase. Using Symbol.for(...) makes sense to me, I'll change this in the next patch version.

I'd love to make TS-Pattern better support custom matchers too. As you've noticed, v5 includes an experimental version of it, but I'd like to make it fully support P.select and beta test it with a few people before I start considering it stable. If you're interested in giving feedback, I can let you know when I have a version I'm satisfied with :)

WoLfulus commented 1 year ago

I'd love to give it a shot! I understand that what I'm asking isn't something a regular user normally does, but it cleaned up a lot of my codebases, and I'm trying to keep native support to it in all my libraries. What I'm asking are just some of the hard parts I had to build ugly workarounds to make it work :P

Let me know how to proceed and I'm more than happy to help!