Repository navigation
fix: make ZodType augmentation resilient to competing zod/v4 augmentations - #378
Open
CodeWithAlexander wants to merge 1 commit into
Open
CodeWithAlexander wants to merge 1 commit into
CodeWithAlexander wants to merge 1 commit into
Conversation
Author
|
UPDATE Reproduction You need three ingredients to trigger this:
Minimal repro using a synthetic stand-in for 3: mkdir repro && cd repro && npm init -y
npm install zod@4.3.6 @asteasolutions/zod-to-openapi@8.5.0 \
@hono/zod-openapi@1.2.4 langchain@1.3.1 typescript@5.9.3
mkdir -p node_modules/trigger
echo '{ "name": "trigger", "version": "1.0.0", "types": "index.d.ts", "main": "index.js" }' > node_modules/trigger/package.json
echo 'module.exports = {};' > node_modules/trigger/index.js
echo 'export * from "zod/v4";' > node_modules/trigger/index.d.tstsconfig.json: {
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"exclude": ["node_modules"]
}index.ts: import { z } from "@hono/zod-openapi";
import { createAgent } from "langchain";
import "trigger";
const test = z.string().openapi({
param: { name: "id", in: "path", required: true },
});npx tsc --noEmit
# TS2339: Property 'openapi' does not exist on type 'ZodString'.Remove any one of the three imports and it compiles fine. The conflict comes from within |
CodeWithAlexander
force-pushed
the
fix/zod-v3-compat-module-augmentation
branch
2 times, most recently
from
April 8, 2026 22:24
3c92f04 to
255ffa2
Compare
…tions
The previous augmentation restated zod's generic constraints, defaults and
heritage clause. TS2428 requires an augmenting declaration's restated
constraints to be identical to the original's, and that identity check fails
once another package (e.g. @langchain/langgraph) merges the same ZodType
interface through the 'zod/v4' module path in the same compilation. The whole
interface merge then collapses and .openapi() disappears.
The augmentation now:
- omits constraints, defaults and the heritage clause, so TS2428's identity
check never applies (it only compares constraints declared on both sides)
- is declared through both the 'zod' and 'zod/v4' module paths, since which
declaration flavor each specifier binds to varies by moduleResolution mode
and TypeScript version
- force-loads 'zod/v4' via an emit-free 'export type {}' re-export, because
TypeScript does not load modules referenced only by declare-module
augmentations
The tsconfig paths entry pins the repo's own node10 compilation to the .d.ts
flavor of zod/v4, mirroring the existing zod/core mapping.
CodeWithAlexander
force-pushed
the
fix/zod-v3-compat-module-augmentation
branch
from
August 5, 2026 07:39
255ffa2 to
8535835
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
.openapi()disappears (TS2339: Property 'openapi' does not exist on type 'ZodString') when three things share one compilation:@asteasolutions/zod-to-openapi(or@hono/zod-openapi)langchain, which pulls in@langchain/langgraphzod/v4subpath (e.g.better-auth, whose.d.tsfiles containexport * from "zod/v4")Remove any one and it compiles fine. Minimal repro in the comment below (a one-line synthetic package stands in for ingredient 3).
Root cause
It is not a conflict inside zod itself.
@langchain/langgraphalso augments zod'sZodType:'zod'and'zod/v4're-export the sameZodTypeinterface, so zod's declaration, langgraph's augmentation and this library's augmentation all merge onto one symbol, but through two different module paths.TypeScript's declaration-merging rule (TS2428) requires an augmenting declaration that restates a type parameter's constraint or default to restate it identically. Our augmentation restated all of zod's constraints and defaults (
Internals extends core.$ZodTypeInternals<...> = ...). Once the interface is merged through a second module path, that identity check fails (the constraint types resolve through different merge chains and are no longer "identical") and the whole merged interface collapses, taking.openapi()with it. With--skipLibCheck falsethe underlying errors are visible at the augmentation site:Notably, langgraph's own second augmentation (
declare module "zod"inplugin.d.ts) survives, because it declaresinterface ZodType<Output>with no constraints and no defaults. TS2428 only compares constraints and defaults declared on both sides. That leniency is the fix.Fix
Drop the restated constraint, default and heritage clause from the augmentation:
interface ZodType<out Output, out Input>. They were redundant restatements of zod's own declaration and are exactly what TS2428 trips over.Input-typed metadata and thethisreturn type are unchanged. (The type parameter names must still match zod's positionally, since TS2428 compares those by name.)Declare the augmentation through both
'zod'and'zod/v4'. Which declaration file each specifier binds to depends on the consumer'smoduleResolutionmode and TypeScript version (for example, node10 resolveszod/v4toindex.d.ctson TS 5.5 butindex.d.tson TS 5.8), so a single path cannot cover all consumers. The'zod/v4'block is what keeps.openapi()alive in the collision scenario above.Force
zod/v4into consuming compilations withexport type {} from 'zod/v4';. TypeScript does not load a module that is referenced only by adeclare moduleaugmentation (the augmentation silently dies with TS2664 otherwise). This re-export survives declaration emit, emits no runtime code, and adds nothing to the API surface.Repo-internal: a
pathsentry pinningzod/v4to./node_modules/zod/v4/index.d.tsfor this repo's own node10 compilation, mirroring the existingzod/coremapping. TypeScript 5.6 and older have a checker quirk where a second augmentation of the same symbol through a second specifier gets its type parameters appended rather than unified; the mapping keeps the two blocks on separate declaration flavors for the repo's pinned TS 5.5.4. Consumers are unaffected by this file.This deletes the old comment "This should always perfectly match the zod type definition ... in terms of generics". The reversal is deliberate, and the new comment documents why.
Validation
Repo gate (what CI runs:
npm ci && npm run build && npm run lint && npm test, zod 4.0.5, TS 5.5.4 / tsd's TS 5.8.3):Original repro (zod 4.3.6, langchain 1.3.1, trigger package; both direct-import and
@hono/zod-openapivariants):TS2339before)Consumer matrix with the packed tarball, covering plain usage, generic
z.ZodType-constrained code, and directimport { z } from 'zod/v4':Metadata typing strictness is unchanged relative to the published 8.5.0 (verified on identical probes). No runtime changes; the diff is entirely type-level.
Known limitation: on TS 5.6 and older with node10 resolution, a compilation that also merges
ZodTypethrough a second module path can still hit the old checker's merge quirk. Plain consumers on TS 5.5.4/node10 test clean; the collision scenario realistically requires a newer TypeScript anyway (the langchain 1.x ecosystem does).Environment
zod: 4.0.5 (repo) / 4.3.6 (repro)@asteasolutions/zod-to-openapi: 8.5.0 baselangchain: 1.3.1