Skip to content

finding(types): WalkableDef declares rest?: z.ZodType but zod 4 mints null there — the inaccurate declaration is what licensed objectui#9088 #9491

Description

@claude

packages/types/src/zod/node-derivation.ts declares the shared walker def type:

export interface WalkableDef {
  type: string;
  items?: z.ZodType[];
  rest?: z.ZodType;
  ...
}

rest?: z.ZodType means z.ZodType | undefined. Zod 4.4.3 does not put undefined there — it puts null.

The measurement

zod 4.4.3, zod/v4/classic/schemas.cjs, the tuple factory:

const rest = hasRest ? _paramsOrRest : null;

Probed directly:

z.tuple([z.number(), z.number()])._zod.def
  def keys              : ["type","items","rest"]
  hasOwn(rest)          : true
  def.rest === null     : true
  def.rest === undefined: false

A census of every null literal in zod v4's classic/schemas.cjs + core/schemas.cjs returns 30 occurrences, of which exactly one is minted into a def member a walker reads — that rest. Every other one is an instance-level accessor default (inst.format, inst.minValue, …), a parse-time value, or unrelated. Swept per member across the twelve the walkers read (items, rest, options, element, shape, valueType, left, right, in, out, innerType, getter), rest is the only one with a non-zero count.

Why it is worth a card rather than a note

This inaccurate declaration is the root cause of objectui#9088, not a cosmetic drift. The tuple arm was written

const rest = def.rest ? walk(def.rest) : undefined;

and undefined is exactly what the declared type says the absent case is. The author read the type, the type was wrong, and unchanged compares by === — so null === undefined was false and every rest-less tuple was rebuilt. objectui#9088 fixes that one arm by copying def.rest instead of normalising it; it does not correct the declaration, because node-derivation.ts is outside that card's declared file surface.

⇒ the next arm written against this type is licensed to make the same mistake, and tsc will agree with it.

Candidate repair

rest?: z.ZodType | null, and then check whether any read of def.rest in either walker needs adjusting. Both current readers (imported-defaults.ts's tuple arm and strict-authoring-face.ts) already guard with a truthiness test, so the widening is expected to be declaration-only — but that is a claim to measure, not to assert. def.out was measured clean in objectui#9088 and needs no change.

Scope note

Found while implementing objectui#9088. Not fixed there: packages/types/src/zod/node-derivation.ts is not on that card's file surface, and the widening touches a type shared by two walkers, so it deserves its own review rather than riding in on a one-arm fix.

Filed by the objectui domain:spec developer seat while implementing objectui#9088. Generated by Claude Code, session session_01L5xpA5q533BgTTNADibEFt.


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanepackage: typespriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions