-
-
Notifications
You must be signed in to change notification settings - Fork 135
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
Add Variant types #45
base: main
Are you sure you want to change the base?
Conversation
Wonderful idea but I feel like it's out of scope from what I feel like a better way to do this would be to help people create bridges between Example: you want to use This way you can focus on having the best pattern matching library and delivering a way for people to extend it at their will :) |
@FredericEspiau I like what you're suggesting in principle. Can you roughly outline how you'd envision a plugin for What I'm not clear on is how you'd make it so I do like this Variants implementation, though I'd be hesitant to adopt it throughout my code. For example, for my work I deal with a lot of objects representing domains (https://instantdomainsearch.com) and I need to track our internal understanding of their 'types' by inspecting various properties. At the moment I can pattern match and get great results, but it's very verbose at times. It could be much nicer if far upstream in my code I could pattern match as the domains come from our back-end and then create variants from them there. Anywhere further downstream I could match on simpler but still reliable expressions. Having said that, I'd be using a |
👋 Thanks for reading the PR and giving your opinion, it's great to have some feedback on this stuff :) Since TS-Pattern uses predicates to determine if a pattern matches or not, you can already use it with your library of choice pretty easily. For example, you can write a const matchZodSchema = <S extends z.Schema<any, any>>(schema: S) =>
when((obj: unknown): obj is z.infer<S> => schema.safeParse(obj).success); and then use it like so: const userSchema = z.object({
name: z.string()
});
const getUserName = (u: unknown) =>
match(u)
.with(matchSchema(userSchema), ({ name }) => name)
.otherwise(() => "Anonymous"); https://codesandbox.io/s/example-ts-pattern-zod-h0z2q I'm not sure we need a "plugin system" for that. A predicate function is enough (but admittedly those predicates can be hard to type correctly).
I understand the wariness about using TS-Pattern specific data constructors throughout a big codebase, that's why I would like those variants to not be specific to the library. My goal is to make it easier to write discriminated union types by automating the creation of constructors via the I decided to open this PR because I feel like most of the variant libraries out there are too complex and not composable enough. From my exploration, it looks like most of them return class instances with methods on them to match on their different cases (something like |
@gvergnaud Thank you for explaining this for me - I understand much better now. I can see that a The more I look at it and consider how I'd use it in my own work, I like it quite a bit. Our use case would be the one I mentioned with creating domain variants and then leveraging them as far upstream in our logic as possible such that we can gradually avoid writing code which works on any type of domain yet is intended for a specific type of domain. It creates an astounding surface area for bugs. This is a sore spot in our code base with many possible solutions, but it's actually why we adopted What are the issues you currently see with the PR? Do you still want to improve upon it, or are there blocking issues? |
55f96e3
to
41c02f4
Compare
d86f4e2
to
6a0a9e0
Compare
Is there interest in making this a sub import so that it doesn't affect bundle size? something like Or, is this easily done is userland nowadays? |
Motivation
TS-Pattern's goal is to make it possible to pattern-match on any kind of data structure, which means you can start using it in your project without having to change your internal data format.
That said, discriminated union types are a bit verbose when compared to data constructors that exist on languages like OCaml, Haskell, ReScript, etc:
In TS
We are repeating
{ tag: "<Name>", value: <Value> }
a lot. In ReScript in comparison, the same code feels let verbose:It looks a lot better because each variant in the union has a matching constructor: a function of same name that takes the variant's content and returns a value of this type.
Solution
Here is a proposal to add an easier way to define Variants and match on them with TS-Pattern:
Variant<Tag, Content>
is a Generic returning a plain object type with this shape:{ tag: Tag, value: Content }
.implementVariants
takes a union type parameter and generate the constructors for each variant of this union. Constructors are simple functions taking the content, and wrapping them in an object with the appropriate tag.The objects constructed using a Constructor function have nothing special. They are plain object of shape
{ tag, value }
, they don't have any method and don't inherit from any class. They can be serialised and constructed by hand.Constructors can also be used to construct a pattern. In this case the returned value will be of type
VariantPattern<Tag, Content>
, which is an alias forVariant<Tag, Pattern<Content>>
. This means you can match with nested constructors as well:Using Variants with ts-pattern would be entirely optional!
match
will keep working on regular values. This isn't a breaking change and implementing this new feature actually doens't require changing the implementation ofmatch
.Inspiration
This addition is in large parts inspired by https://github.com/practical-fp/union-types. Using a typed Proxy to generate constructors from a union type is a very good idea. Unfortunately the values created with
union-types
aren't compatible withts-pattern
, this PR tries to bridge this gap