Skip to content

test(v4): cover object union property inference#5978

Open
ShikShot wants to merge 1 commit into
colinhacks:mainfrom
ShikShot:bounty/zod-2654-object-union-regression
Open

test(v4): cover object union property inference#5978
ShikShot wants to merge 1 commit into
colinhacks:mainfrom
ShikShot:bounty/zod-2654-object-union-regression

Conversation

@ShikShot
Copy link
Copy Markdown

Summary

  • Adds Zod 4 regression coverage for the object-property union inference case reported in Schema in object being inferred differently (and weirdly) #2654.
  • Checks that the original z.string().or(z.array(z.string())) object property infers the same output type as the standalone union schema.
  • Adds adjacent coverage for the array-or-record and enum-or-record cases reported in the issue thread.

Validation

  • corepack pnpm vitest run packages/zod/src/v4/classic/tests/object.test.ts
  • Pre-push full suite: 339 test files, 3813 tests, no type errors

Refs #2654
/claim #2654

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Consider whether the first sub-block is worth keeping given existing coverage in generics.test.ts. The test is otherwise sound.

TL;DR — Adds a regression test asserting that z.infer<> of a union-typed object property matches the standalone union's inferred type, locking in the Zod 4 fix for #2654. Type assertions are correct; one sub-block partially duplicates existing coverage.

Key changes

  • Object-property union inference regression test — Covers the original string | string[] repro plus adjacent array-or-record and enum-or-record-with-optional cases from the issue thread.

Summary | 1 file | 1 commit | base: mainbounty/zod-2654-object-union-regression

packages/zod/src/v4/classic/tests/object.test.ts


Verified the asserted shapes

Before: No test pinned the in-object-vs-standalone equivalence for these union shapes.
After: Three sub-blocks assert both toEqualTypeOf<EventName>() (cross-context equivalence) and the literal shape (string | string[], string[] | Record<string, string>, the enum-or-record union with undefined).

The dual-assertion pattern is good defense in depth — a regression that broke both contexts identically would slip past the cross-equivalence assertion alone, but the literal-shape assertion would still catch it.

The third block's asserted Record<string, "Administrator" | "Writer" | "Readonly" | undefined> is the literal Zod 4 output: $InferZodRecordOutput at packages/zod/src/v4/core/schemas.ts:2834-2839 returns Record<output<K>, output<V>> for non-$partial keys, and $ZodOptional outputs output<T> | undefined. So the test will pass under tsc.


Partial overlap with generics.test.ts

Before: generics.test.ts:51-58 ("nested no undefined") already covers z.string().or(z.array(z.string())) inside z.object({ inner }) and asserts the resulting shape.
After: The new block 1 (lines 250-259) repeats this. Blocks 2 and 3 (lines 261-284) are net-new coverage.

Not a blocker — co-locating the regression with object.test.ts and explicitly contrasting standalone vs in-object inference has documentary value. But block 1 could be trimmed without losing protection. Up to you.

packages/zod/src/v4/classic/tests/generics.test.ts

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment on lines +250 to +259
const EventNameSchema = z.string().or(z.array(z.string()));
const EventSchema = z.object({
name: z.string().or(z.array(z.string())),
});

type EventName = z.infer<typeof EventNameSchema>;
type EventNameFromObject = z.infer<typeof EventSchema>["name"];

expectTypeOf<EventNameFromObject>().toEqualTypeOf<EventName>();
expectTypeOf<EventNameFromObject>().toEqualTypeOf<string | string[]>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sub-block duplicates generics.test.ts:51-58 ("nested no undefined"), which already pins z.string().or(z.array(z.string())) inside a z.object({ inner }) to { inner: string | string[] }. Not a blocker — the explicit standalone-vs-property contrast adds documentary value — but worth being aware of in case you'd rather trim it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant