Forcing a Single Dependency Version with pnpm overrides
Two packages pull in different versions of the same transitive dependency, causing duplicate copies and subtle bugs. This page shows how to force one version across the whole pnpm workspace with pnpm.overrides.
Exact symptoms and error messages
$ pnpm why zod
├─┬ pkg-a 1.0.0
│ └── zod 3.22.0
└─┬ pkg-b 2.0.0
└── zod 3.23.8
# two copies of zod in one tree
Root cause analysis
Different ranges that do not overlap force pnpm to install two copies, which duplicates module identity and can break instanceof/shared-schema checks. When you know one version is compatible for all consumers, an override collapses the graph to a single copy — the deduplication mechanic explained in Dependency Resolution Explained and applied across a workspace via the workspace configuration.
Duplicate copies arise when two packages depend on ranges that do not overlap — zod@^3.22 and zod@3.23.8 pinned exactly, say — so pnpm cannot satisfy both with one version and installs two. For stateless utilities this is merely wasteful disk; for packages whose identity matters (a shared schema instance, a class checked with instanceof), two copies mean two identities and subtle, hard-to-trace bugs. An override collapses the graph to one version when you know a single version is compatible for all consumers.
The risk you accept is exactly the compatibility you are asserting. An override rewrites the resolved version regardless of what each parent requested, so if one parent genuinely needed the old version, forcing the new one can break it. That is why an override is a claim — 'this version satisfies everyone' — that you should verify with the workspace's tests rather than assume.
Resolution and configuration patch
Declare the pinned version in the root package.json:
{
"pnpm": {
"overrides": {
"zod": "3.23.8"
}
}
}
Scope it narrowly (e.g. "pkg-a>zod": "3.23.8") if only one dependency path should be forced. Re-run install to regenerate the lockfile against the single version.
CLI validation and debug commands
# Confirm a single version now resolves
pnpm why zod
# Regenerate and verify the lockfile
pnpm install --frozen-lockfile
# Check no consumer breaks against the forced version
pnpm -r test
Prevention and CI guardrails
- Force a single version only when you have verified compatibility across consumers.
- Prefer a scoped override (
parent>dep) over a global one where possible. - Remove the override once upstream ranges converge naturally.
- Re-run the workspace test suite after forcing to catch a version-specific break.
Scoping an override to the narrowest path
A global override is the biggest hammer: it forces a version everywhere the package appears. Often you only need it in one subtree, and pnpm lets you scope the override to a specific dependency path so the rest of the graph keeps its originally-resolved versions.
{
"pnpm": {
"overrides": {
"pkg-a>zod": "3.23.8"
}
}
}
The scoped form pkg-a>zod forces the version only under pkg-a, leaving any other consumer of zod untouched. Preferring the narrow form reduces the blast radius of the compatibility claim you are making — you assert that pkg-a works with the forced version, not that the entire ecosystem does. Reach for the global form only when every path genuinely must move together.
Overrides versus a workspace catalog
pnpm offers two related tools that solve different problems. An override forces a resolved version, including transitive dependencies you do not declare — the right tool for collapsing a duplicated sub-dependency. A catalog centralizes the versions your own workspace packages declare for their direct dependencies, so every package references catalog: instead of a hard-coded range and they stay in lockstep.
Use a catalog to keep direct dependencies consistent across your packages — one place to bump react for the whole workspace — and reserve overrides for the stubborn transitive duplicates a catalog cannot reach. Mixing them appropriately keeps your declared versions tidy through the catalog while overrides handle the graph-level forcing, rather than overloading overrides to do both jobs and losing the clarity of which versions you actually chose.
Frequently Asked Questions
Does an override change what my published package requires?
No — pnpm.overrides affects your install tree only, not the ranges your published package declares. Consumers resolve their own graph independently.
Override vs a catalog for shared versions?
An override forces a resolved version, even for transitive deps. A catalog (pnpm) centralizes the versions your own packages declare. Use a catalog for direct deps and an override for stubborn transitive duplicates.
How narrow should a pnpm override be?
As narrow as clears the duplication. Use the scoped form parent>dep when only one subtree is affected so other consumers keep their resolved versions, and reserve the global form for when every path must move to the pinned version together.
What's the difference between an override and a catalog?
An override forces a resolved version, including transitive dependencies. A catalog centralizes the versions your own workspace packages declare for their direct dependencies. Use a catalog for consistency and overrides for stubborn transitive duplicates.
Does forcing a version risk breaking a consumer?
Yes — an override rewrites the resolved version regardless of what each parent requested, so if a parent needed the old version it can break. Treat the override as a compatibility claim and verify it with the workspace test suite.
Related
- Dependency Resolution Explained — how ranges and the graph decide installed versions.