Specify a Zod schema with a non-optional but possibly undefined field
Asked Answered
S

3

27

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.

Steel answered 15/3, 2022 at 4:18 Comment(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).Masteratarms
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 undefinedSteel
F
19

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 }
Forestay answered 2/12, 2022 at 2:30 Comment(2)
@Steel I know this is old, but would it have solved your problem?Forestay
Really wish I could figure out how to create a reusable function to do this. Or that Zod had something like .undefinable() and not just .optional()Mcguire
M
0

Building off the answer from Gus Bus... If you want every property to be required (non-optional), I made a utility function to make this a little easier.

export type Full<T> = { [K in keyof T]-?: [T[K]] } extends infer U
  ? U extends Record<keyof U, [any]>
    ? { [K in keyof U]: U[K][0] }
    : never
  : never;

/** Marks every property as required (non-optional). However property values can still be undefined. */
export function full<T>(x: T) {
  return x as Full<T>;
}

Usage:

const schema = z
  .object({
    name: z.string().optional(),
  })
  .transform(full);

Before (no transform):

enter image description here

After (transform):

enter image description here

Reference: https://mcmap.net/q/535196/-how-to-prevent-required-lt-t-gt-in-typescript-from-removing-39-undefined-39-from-the-type-when-using-strictnullchecks

Mcguire answered 19/4, 2024 at 16:10 Comment(6)
This looks like it's only half of the solution because it's only handling the type inference side of things. You need to make sure that when you parse the object that the fields are being set.Forestay
I think you're right. But wouldn't your original answer have the same issue? I'm still working on this. I think the validation needs to check that the entire object has certain keys, and not necessarily do prop validation to find missing props.Mcguire
I will try this tomorrow, and if it works I'll update the answer. #77958964Mcguire
The solution in my original post would convert an empty object to an object with {blah: undefined}. Your solution would leave it as empty, all you're doing is type casting. Which is fine if that's what you want - I was just pointing out there is a material difference between the two solutions.Forestay
Thanks. In my use case, I'm wanting to warn developers that they forgot to include a key.Mcguire
I haven't been able to figure it out, and I have to pause on this for now. Thanks.Mcguire
L
-5

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
Landward answered 1/12, 2022 at 14:47 Comment(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.Miscible

© 2022 - 2025 — McMap. All rights reserved.