zod
zod copied to clipboard
Add pattern matching to unions
Pattern matching in Unions
Overview
This PR adds a pattern matching function to ZodUnions
, similar to Runtypes
The code is a bit complex so I can go through it in detail.
The match
function receives a rest parameter of the same length as the tuple that defined the union type. Let's say you have the following ZodUnion
:
z.union(z.string(), z.number(), z.bigint(), z.literal('a'))
then, the match
function will receive four arguments, ordered.
For each argument, you have two options:
- Pass only a function that receives the
output
of the schema in that position and can return whatever you'd like. - Or you can pass an object with a
key
and acheck
function. You can usekey
to name your case. Thecheck
key should receive the same function as described in item 1.
So, for example:
// For this union:
const myUnion = z.union(z.string(), z.number(), z.bigint(), z.literal('a'))
const isGreaterThanTwo = myUnion.match(
(str) => str.length > 2,
(num) => num > 2,
{ key: "my bigint", check: (bi) => false },
{ key: "my literal", check: () => false },
)
This will return another function that you can use to see if an input matches one of the patterns. In this example, the function returned would have the following signature:
(x: unknown /* the input */ ) => {
key: 0, value: boolean
} | {
key: 1, value: boolean,
} | {
key: "my bigint", value: boolean
} | {
key: "my literal", value: boolean
}
Here you can notice that unnamed cases receive the array index as key
, while named cases get identified back.
All return types and case names get inferred correctly, and you can mix and match return types freely.
const anotherExample = myUnion.match(
(str) => str.length > 2,
(num) => `${num}`,
{ key: "my bigint", check: (bi) => { an: "object" } },
{ key: "my literal", check: () => [0, 1] },
)
// which returns:
(x: unknown) => {
key: "0";
value: boolean;
} | {
key: "1";
value: string;
} | {
key: "my bigint";
value: {
an: string;
};
} | {
key: "my literal";
value: number[];
}
Last but not least, the return type is a discriminated union with key
as the discriminator, so you can make use of this to know at which case your function matched.
Next steps
(doesn't have to be in this PR)
- [ ] Add catchall support
Deploy Preview for guileless-rolypoly-866f8a ready!
Built without sensitive environment variables
Name | Link |
---|---|
Latest commit | 52019de704f2e81e62a0c463927ad30683b08004 |
Latest deploy log | https://app.netlify.com/sites/guileless-rolypoly-866f8a/deploys/63470f4b1966d20009fb52da |
Deploy Preview | https://deploy-preview-1483--guileless-rolypoly-866f8a.netlify.app |
Preview on mobile | Toggle QR Code...Use your smartphone camera to open QR code link. |
To edit notification comments on pull requests, go to your Netlify site settings.
I've added some specs, it will help with understanding the logic.
@santosmarco-caribou I'm not sure to understand your match function. Can't you use transform functions on the items of your union for your use case ? It would result in something like this:
// For this union:
const isGreaterThanTwo = z.union(
z.string(). tranform(str => {key: 0, check: str.length > 2}),
z.number(). tranform(num => {key: 1, check: num > 2}),
z.bigint(). tranform(() => {key: "my bigint", check: false}),
z.literal('a'). tranform(() => {key: "my literal", check: false})
)
@roblabat i believe what's requested here a smarter type-safer switch-case statement, ie pattern-matching.
your example defines a union to create a utility function. what if we wanted to check isGreaterThanTwo at somewhere and also isLessThanFive at somewhere else. we would need 2 different unions.
the proposed solution, uses a single union, and match is a utility function.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.