Schema in object being inferred differently (and weirdly)
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
💎 $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!
Add "strictNullChecks": false to tsconfig.json
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
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); }
#2654
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
@ericallam has this issue been resolved if not can you assign it to me
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 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.
/attempt #2654
I can work on this ticket I can be assigned @ericallam
/attempt #4030
Hi @ericallam , is this still open to work on?
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