Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions zod/src/__tests__/subpath-imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { readFileSync } from 'fs';
import { join } from 'path';

// `zod/v3` and `zod/v4/core` don't exist before zod@3.25.0. Importing either
// as a value (rather than `import type`) makes the compiled output reference
// that subpath at runtime, which breaks module resolution for anyone on an
// older zod 3.x — even if they never touch the zod4 code path (#811).
test('zod/v3 and zod/v4/core are only ever imported as types', () => {
const source = readFileSync(join(__dirname, '..', 'zod.ts'), 'utf8');

const v3Import = source.match(/^import .*'zod\/v3';$/m)?.[0];
const v4Import = source.match(/^import .*'zod\/v4\/core';$/m)?.[0];

expect(v3Import).toMatch(/^import type /);
expect(v4Import).toMatch(/^import type /);
});
17 changes: 13 additions & 4 deletions zod/src/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import {
ResolverSuccess,
appendErrors,
} from 'react-hook-form';
import * as z3 from 'zod/v3';
import * as z4 from 'zod/v4/core';
// Type-only: these subpaths don't exist before zod@3.25.0, so importing them
// as values would break resolution for anyone on an older zod 3.x. `import
// type` is fully erased at build time, so the compiled output never
// references these subpaths at runtime — see `isZod4Error` below for how the
// one runtime check that used to need `z4.$ZodError` avoids that now.
import type * as z3 from 'zod/v3';
import type * as z4 from 'zod/v4/core';

const isZod3Error = (error: any): error is z3.ZodError => {
return Array.isArray(error?.issues);
Expand All @@ -21,8 +26,12 @@ const isZod3Schema = (schema: any): schema is z3.ZodSchema => {
return '_def' in schema && typeof schema._def === 'object';
};
const isZod4Error = (error: any): error is z4.$ZodError => {
// instanceof is safe in Zod 4 (uses Symbol.hasInstance)
return error instanceof z4.$ZodError;
// Replicates zod4's own `$ZodError`/`Symbol.hasInstance` check
// (`inst?._zod?.traits?.has(name)`, see zod's `$constructor` in
// `v4/core/core.ts`) by hand rather than importing `$ZodError` as a value,
// since that import only exists in zod@3.25.0+ (see the `import type` note
// above).
return !!error?._zod?.traits?.has('$ZodError');
};
const isZod4Schema = (schema: any): schema is z4.$ZodType => {
return '_zod' in schema && typeof schema._zod === 'object';
Expand Down
Loading