Skip to content

Handling discriminated unions #16

@joshuaslate

Description

@joshuaslate

Hi, thanks for your work on this library, it's been super helpful in making some of the more complex/deeply-nested forms I work with more type-safe.

I saw that there's a PR open for handling optional fields, which was one of the two pain points I've hit so far, so I'm excited for that to land. The other is for discriminated unions.

I've been using a pattern, like this (for example):

import * as z from 'zod';

const formSchema = z.object({
  name: z.string(),
  animal: z.discriminatedUnion('type', [
    z.object({ type: z.literal('dog'), value: z.object({ canBark: z.boolean() }) }),
    z.object({ type: z.literal('cat'), value: z.object({ canMeow: z.boolean() }) }),
  ]),
});

Effectively, the Animal value type is:

{ type: 'dog', value: { canBark: boolean } } | { type: 'cat', value: { canMeow: boolean } }

Many of the APIs I work with use a number of "oneOf" values, so I lean into these pretty heavily. Often, I will need to conditionally show different fields based on the selected type (in this example, animal.type). I've put together a little CodeSandbox that shows the issues I'm running into: https://codesandbox.io/p/sandbox/hrymxc

I think there are two main issues with the approach I've taken here (maybe there's a better way?):

  1. I'm not able to focus the type path from a Lens that was focused to animal before without a TypeScript error:
This expression is not callable.
  Each member of the union type '(<P extends "type" | "value" | "value.canBark">(path: P) => Lens<P extends `${infer K}.${infer R}` ? K extends requiredKeys<baseObjectOutputType<{ type: ZodLiteral<"dog">; value: ZodObject<{ canBark: ZodBoolean; }, "strip", ZodTypeAny, { ...; }, { ...; }>; }>> ? PathValueImpl<...> : K extends `${number}` ? never : n...' has signatures, but none of those signatures are compatible with each other.typescript(2349)

Interestingly, I noticed that I'm not getting the TS error if I lens.focus('animal.type') or even lens.focus('animal.value.canBark') from the root-level lens-- just the one that's already been focused to animal.

  1. I'm trying to type narrow. I'm using useWatch to narrow based on the form value that the user selected (so I can show the appropriate inputs for their selection), but the lens, of course, isn't narrowed along with it.

Again, I really appreciate your work on this library, it's been a joy to work with so far! Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions