Zod's product is largely its types — z.infer is the reason the library exists — so the TypeScript setup is unusually deliberate.

The compiler pin

The root package.json declares "typescript": "~5.5.4": a tilde range, not a caret, so the repo builds with 5.5.x only. Zod's type-level code (recursive inference, template-literal types, the $replace machinery in registries) sits close to compiler limits, and behavior differences between TypeScript minor versions can change what compiles and how fast. The dedicated workspace packages/tsc (@zod/tsc-perftest) exists precisely to benchmark compiler performance of Zod-heavy code.

The shared compiler options are strict across the board:

.configs/tsconfig.base.json
{
  "compilerOptions": {
    "target": "es2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

exactOptionalPropertyTypes matters more here than in most repos: Zod 4 distinguishes "key absent" from "key present with undefined" (see optional vs exactOptional in the API), and the compiler flag enforces the same distinction in the library's own types.

zshy: one source tree, dual output

The build command is a single line — zshy --project tsconfig.build.json — and zshy (a build tool by Zod's author, in devDependencies at ^0.7.2) reads its configuration from a zshy field in the package manifest:

packages/zod/package.json
"zshy": {
  "exports": {
    "./package.json": "./package.json",
    ".": "./src/index.ts",
    "./mini": "./src/mini/index.ts",
    "./locales": "./src/locales/index.ts",
    "./v3": "./src/v3/index.ts",
    "./v4": "./src/v4/index.ts",
    "./v4-mini": "./src/v4-mini/index.ts",
    "./v4/mini": "./src/v4/mini/index.ts",
    "./v4/core": "./src/v4/core/index.ts",
    "./v4/locales": "./src/v4/locales/index.ts",
    "./v4/locales/*": "./src/v4/locales/*"
  },
  "conditions": {
    "@zod/source": "src"
  }
}

From that map zshy emits, next to each entry point, .js/.cjs plus .d.cts/.d.mts declarations — the generated exports field in the same file shows the result, with import, require, and types conditions per subpath. Output lands in the package root ("outDir": "." in tsconfig.build.json), which is why files whitelists **/*.js, **/*.cjs, **/*.d.cts, and also src — the original TypeScript sources ship in the tarball.

The @zod/source condition

The "conditions": { "@zod/source": "src" } line generates an extra branch in every export: when a tool resolves with the @zod/source condition, import "zod" lands on packages/zod/src/index.ts instead of built output. The whole repo runs on it:

package.json
"dev": "tsx --conditions @zod/source",
"bench": "tsx --conditions @zod/source packages/bench/index.ts",

and the Vitest config sets resolve.conditions: ["@zod/source", "default"]. The practical consequence for contributors: there is no build step in the edit-test loop, ever. The separate packages/integration workspace covers the other side, testing against actual build output (test:built vs test:source).

Runtime code discipline

Two source-level conventions support the toolchain. Nearly every exported constructor is annotated /*@__PURE__*/ (and factories @__NO_SIDE_EFFECTS__) so bundlers can eliminate unused schemas — this is what makes zod/mini meaningfully tree-shakable, and packages/treeshake verifies it. And pnpm check:circular runs madge over packages/zod/src excluding v4/core and v3, keeping the API layers free of import cycles.

Sources: packages/zod/package.json, packages/zod/tsconfig.build.json, .configs/tsconfig.base.json · last synced 2026-08-10 · 2d90846 · version 4.4.3