Skip to content

zod/mini: merge() type and runtime signatures are inverted #6302

Description

@josher8a

z.mini.merge() declares a schema-to-schema signature but forwards to util.extend(), which requires a plain shape. The call that typechecks throws at runtime, and the call that runs is a type error. There's no way to call it that satisfies both.

import * as m from "zod/mini";

const A = m.object({ a: m.string() });
const B = m.object({ b: m.string() });

m.merge(A, B);                  // typechecks; throws at runtime
m.merge(A, { b: m.string() });  // works at runtime; type error TS2353

On zod@4.4.3:

merge(schema, schema) THROWS: Invalid input to extend: expected a plain object
merge(schema, shape) : [ 'a', 'b' ]

Under tsc --noEmit --strict (5.9.3 and 7.0.2 agree):

error TS2353: Object literal may only specify known properties,
  and 'b' does not exist in type 'ZodMiniObject<...>'

Cause

mini/schemas.ts:921 declares only the schema-to-schema overload, so the implementation signature isn't publicly callable:

/** @deprecated Identical to `z.extend(A, B)` */
export function merge<T extends ZodMiniObject, U extends ZodMiniObject>(
  a: T,
  b: U
): ZodMiniObject<util.Extend<T["shape"], U["shape"]>, T["_zod"]["config"]>;
export function merge(schema: ZodMiniObject, shape: any): ZodMiniObject {
  return util.extend(schema, shape);
}

util.extend guards on isPlainObject and throws (core/util.ts:659).

Mini only. Classic calls util.merge (classic/schemas.ts:1555) and A.merge(B) works. Present since the Zod 4 commit 8592854, unchanged in 4.4.3. mini/tests/object.test.ts has no merge case.

Two fixes, not equivalent

Correcting the overload to (schema, shape) matches the @deprecated Identical to z.extend(A, B) note above it, with no runtime change. It breaks merge(A, B) at compile time, but that call already throws, so the failure just surfaces earlier.

Switching the body to util.merge makes the advertised signature true, matches classic and Zod 3, and picks up b's catchall. It breaks JS callers passing a plain shape, which is the form that works today. Not silently: checks: b._zod.def.checks ?? [] (core/util.ts:713) is eager, so it throws TypeError: Cannot read properties of undefined (reading 'def') at the call. Still a runtime change to a shipped API.

merge is deprecated and absent from api.mdx, so the first is the conservative read. Which would you prefer? Happy to open a PR with a test either way.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions