14

Is it possible to define a Zod schema with a field that is possibly undefined, but non-optional. In TypeScript this is the difference between:

interface IFoo1 {
  somefield: string | undefined;
}

interface IFoo2 {
  somefield?: string | undefined;
}

const schema = z.object({
  somefield: z.union([z.string(), z.undefined()]),
}); // Results in something like IFoo2

As far as I can tell using z.union([z.string(), z.undefined()]) or z.string().optional() results in the field being equivalent to IFoo2.

I'm wondering if there is a way to specify a schema that behaves like IFoo1.

Context / Justification

The reason that I might want to do something like this is to force developers to think about whether or not the field should be undefined. When the field is optional, it can be missed by accident when constructing objects of that type. A concrete example might be something like:

interface IConfig {
  name: string;
  emailPreference: boolean | undefined;
}
enum EmailSetting {
  ALL,
  CORE_ONLY,
}

function internal(config: IConfig) {
  return {
    name: config.name,
    marketingEmail: config.emailPreference ? EmailSetting.ALL : EmailSetting.CORE_ONLY,
  }
}

export function signup(userName: string) {
  post(internal({ name: userName }));
}

This is sort of a contrived example, but this occurs a lot in our codebase with React props. The idea with allowing the value to be undefined but not optional is to force callers to specify that, for example, there was no preference specified vs picking yes or no. In the example I want an error when calling internal because I want the caller to think about the email preference. Ideally the type error here would lead me to realize that I should ask for email preference as a parameter to signup.

Souperman
  • 5,057
  • 1
  • 14
  • 39
  • 2
    Have you considered using `null` instead of `undefined`? I think it'd be also more explicit as these two values were made to distinguish between a variable not being set/initialised yet (`undefined`) and a variable currently without a specific value (`null`). – knoefel Mar 20 '22 at 17:08
  • 1
    That’s a reasonable suggestion, and probably what I’ll do if there’s no work around, but for my specific use case I was hoping to use `undefined` to represent the absence of a configuration where `null` is a possible configuration. If I pass `null` in to mean “no preference” as well as a possible “preference is `null`“ then there’s a bit of overloading going on. I suppose I could use a bonafide optional type but I was hoping to simply use `undefined` – Souperman Mar 21 '22 at 02:47

2 Answers2

10

You can use the transform function to explicitly set the field you're interested in. It's a bit burdensome, but it works.

const schema = z
    .object({
        somefield: z.string().optional(),
    })
    .transform((o) => ({ somefield: o.somefield }));

type IFoo1 = z.infer<typeof schema>;
// is equal to { somefield: string | undefined }
Gus Bus
  • 369
  • 3
  • 7
-2

It seems like z.optional() is what you're looking for based on the docs: https://github.com/colinhacks/zod#optionals

const schema = z.optional(z.string());

schema.parse(undefined); // => returns undefined
type A = z.infer<typeof schema>; // string | undefined
Chris Lim
  • 56
  • 8
  • 1
    OP is not asking how to make a schema accept `undefined`. The question is whether there "*is a way to specify a schema that behaves like `IFoo1`*", specifically with its `somefield` property being present but `undefined`. – Bergi Dec 01 '22 at 20:32