zod icon indicating copy to clipboard operation
zod copied to clipboard

Add pattern matching to unions

Open santosmarco-caribou opened this issue 2 years ago • 4 comments

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:

  1. Pass only a function that receives the output of the schema in that position and can return whatever you'd like.
  2. Or you can pass an object with a key and a check function. You can use key to name your case. The check 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

santosmarco-caribou avatar Oct 12 '22 18:10 santosmarco-caribou

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...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

netlify[bot] avatar Oct 12 '22 18:10 netlify[bot]

I've added some specs, it will help with understanding the logic.

santosmarco-caribou avatar Oct 12 '22 19:10 santosmarco-caribou

@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 avatar Oct 28 '22 15:10 roblabat

@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.

utkuturunc avatar Nov 21 '22 23:11 utkuturunc

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.

stale[bot] avatar May 10 '23 00:05 stale[bot]