Published on

Define a Zod Schema with Non-Optional but Possibly Undefined Fields

Authors
  • Name
    Ripal & Zalak
    Twitter

Define a Zod Schema with Non-Optional but Possibly Undefined Fields

Problem

In TypeScript, there's a subtle difference between the following interfaces:

interface IFoo1 {
  somefield: string | undefined
}

interface IFoo2 {
  somefield?: string | undefined
}

Here, IFoo1 ensures that the somefield property is always present, but its value can explicitly be undefined. On the other hand, IFoo2 makes somefield optional, which means it may not exist at all.

Zod Schema for Non-Optional Undefined Field

To create a Zod schema resembling IFoo1, you can use z.string().optional() combined with a transformation to ensure the field is always present but may hold undefined. Here's how:

import { z } from 'zod'

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

// Type inference
type IFoo1 = z.infer<typeof schema>

// Usage example
const validObject = schema.parse({ somefield: undefined }) // Passes validation
const invalidObject = schema.parse({}) // Fails validation

Common Use Case: Configuration Objects

A practical scenario for this pattern is enforcing strictness in configuration objects:

interface IConfig {
  name: string
  emailPreference: boolean | undefined
}

const configSchema = z
  .object({
    name: z.string(),
    emailPreference: z.boolean().optional(),
  })
  .transform((obj) => ({ ...obj, emailPreference: obj.emailPreference }))

type Config = z.infer<typeof configSchema>

This ensures developers explicitly consider the emailPreference field, preventing accidental omission.

FAQs

1. Why not use null instead of undefined?

While null can represent an intentional "no value," undefined often signifies the absence of initialization. Choosing between them depends on your application's conventions.

2. Can this approach be reused for multiple schemas?

Yes, you can encapsulate this logic in a utility function for reusability:

const nonOptional = <T extends z.ZodTypeAny>(schema: T) =>
  schema.optional().transform((value) => value)

const schema = z.object({
  somefield: nonOptional(z.string()),
})

3. What happens if I don't use .transform?

Without .transform, Zod treats the field as optional, meaning it may not exist in the parsed object, which doesn't align with IFoo1 behavior.