colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
31.9k stars 1.11k forks source link

Schema in object being inferred differently (and weirdly) #2654

Open ericallam opened 11 months ago

ericallam commented 11 months ago

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

algora-pbc commented 10 months ago

💎 $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!

kremedev commented 10 months ago

Add "strictNullChecks": false to tsconfig.json

zcesur commented 10 months ago

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

szulcus commented 10 months ago

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); }
VirtualDharm commented 8 months ago

2654

omermizr commented 8 months ago

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

MadhavPujara commented 8 months ago

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

AlexGalays commented 6 months ago

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.

abhi12299 commented 6 months ago

@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