zod icon indicating copy to clipboard operation
zod copied to clipboard

Schema in object being inferred differently (and weirdly)

Open ericallam opened this issue 2 years ago • 13 comments

See the following TS snippet:

import { z } from "zod";

const EventNameSchema = z.string().or(z.array(z.string()));

type EventName = z.infer<typeof EventNameSchema>;
// EventName is string | string[]

const EventSchema = z.object({
  name: z.string().or(z.array(z.string())) // this is the same as the EventNameSchema
});

type EventWithName = z.infer<typeof EventSchema>;
type EventName2 = EventWithName["name"];
// EventName2 is (string | string[]) & (string | string[] | undefined)

And the TS playground: link

I'm not sure if this is intended or a bug or maybe just a user error. Using zod 3.21.4 and TS 4.8.4

ericallam avatar Aug 14 '23 21:08 ericallam

💎 $25 bounty created by @ericallam 👉 To claim this bounty, submit your pull request on Algora 📝 Before proceeding, please make sure you can receive payouts in your country 💵 Payment arrives in your account 2-5 days after the bounty is rewarded 💯 You keep 100% of the bounty award 🙏 Thank you for contributing to colinhacks/zod!

algora-pbc avatar Aug 23 '23 20:08 algora-pbc

Add "strictNullChecks": false to tsconfig.json

kremedev avatar Aug 23 '23 22:08 kremedev

had a similar issue recently, seems to be due to the split & intersect approach for inferring object types

typing addQuestionMarks more precisely does resolve this issue:

 export type addQuestionMarks<
   T extends object,
   R extends keyof T = requiredKeys<T>
- > = Pick<Required<T>, R> & Partial<T>;
+ > = Pick<Required<T>, R> & Omit<Partial<T>, R>;

but also introduces new problems like breaking the inference of generic schemas (as the compiler can no longer determine their shape)

relevant thread by @colinhacks

zcesur avatar Aug 24 '23 04:08 zcesur

I have the same problem:

Works fine:

const test = string().array().or(record(string()));
type Test = z.infer<typeof test> // string[] | Record<string, string>

Don't work:

const test = object({ values: string().array().or(record(string())) });
type Test = z.infer<typeof test>; // { values: (string[] | Record<string, string>) & (string[] | Record<string, string> | undefined); }

szulcus avatar Aug 28 '23 13:08 szulcus

#2654

VirtualDharm avatar Oct 31 '23 04:10 VirtualDharm

Honestly this makes zod unusable for me (using strict TS). Guess I'll stick with v3.21.1 and copy over the email validation schema from v3.22.3 for now

omermizr avatar Nov 08 '23 15:11 omermizr

@ericallam has this issue been resolved if not can you assign it to me

MadhavPujara avatar Nov 14 '23 04:11 MadhavPujara

It's still here in 3.22.4

In particular, our use case is:

const role = z.enum(["Administrator", "Writer", "Readonly"])
const rolesPerLocale = z.record(role.optional())
const repositoryRole = role.or(rolesPerLocale)

// Then later
    z.object({
      role: repositoryRole
    })

This makes us unable to later use typeof role === "string" to discriminate between the basic role and the rolePerLocale because we get this weird, impossible string & {} in the signature.

AlexGalays avatar Jan 04 '24 15:01 AlexGalays

@AlexGalays with the PR #3138 I believe this will get sorted out. This is what I get from running the code you provided. This seems to be the right type inference.

image

abhi12299 avatar Jan 14 '24 17:01 abhi12299

/attempt #2654

Harshit0741 avatar Dec 01 '24 12:12 Harshit0741

I can work on this ticket I can be assigned @ericallam

ghost avatar Dec 24 '24 13:12 ghost

/attempt #4030

Kunal-Darekar avatar Mar 16 '25 11:03 Kunal-Darekar

Hi @ericallam , is this still open to work on?

sparkybug avatar Mar 22 '25 17:03 sparkybug

This is expected behavior in Zod 3.21.4 and TypeScript 4.8.4. When you use .or() (which is just a shortcut for z.union) inside an object schema, Zod's type inference combines the union type with the possibility of undefined (since object properties are optional by default unless you use .required()). This leads to the intersection type (string | string[]) & (string | string[] | undefined), which is a quirk of how TypeScript handles intersections and optional properties in object schemas. You can see this documented in the Zod v3 docs and discussed in related issues: docs, issue 4610.

This was improved in later Zod versions (a fix landed in 3.25.50), but for your version, the best workaround is to explicitly mark the property as required with .required(), or use a type annotation to get the type you want. If this answers your question, please close the issue!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

dosubot[bot] avatar Jul 22 '25 03:07 dosubot[bot]