What Is pnpm?

October 8, 20268 min read
Requirements:
ObjectsFunctions

You cloned a repo, saw a pnpm-lock.yaml instead of the package-lock.json you expected, and now you are here. So: what is pnpm? It is a package manager for Node — a drop-in alternative to npm and Yarn that installs the same packages from the same registry, but stores them on disk in a completely different way.

That storage difference is the whole story. It makes installs faster, saves a lot of disk space, and — the part that actually matters for TypeScript — it makes your dependency graph honest. Packages you never declared stop being importable, which catches a class of bug that npm will happily let you ship.

This page covers how pnpm works, what changes in a TypeScript project, how workspaces behave in a monorepo, and the handful of places where the strictness bites.

What Is pnpm Doing Differently?

npm and Yarn Classic install packages by copying them. Every project on your machine gets its own full copy of every dependency, flattened into one big node_modules directory. Ten projects using the same version of typescript means ten copies on disk.

pnpm installs packages once, into a global content-addressable store, then links them into each project. The eleventh project costs almost nothing — no download, no copy, just links.

Two mechanisms do the work:

The store lives outside your project (run pnpm store path to find it). Deleting node_modules and reinstalling is nearly instant, because nothing has to be fetched again.

The node_modules Layout

This is the part worth understanding, because it explains every surprise you will hit later.

With npm, node_modules is flat. Your direct dependencies and all of their transitive dependencies get hoisted to the top level, side by side. With pnpm, the top level contains only what you declared in package.json. Everything else lives in a hidden node_modules/.pnpm directory and is wired up with symlinks:

node_modules/
  typescript -> .pnpm/typescript@5.9.2/node_modules/typescript
  .pnpm/
    typescript@5.9.2/
    some-transitive-dep@1.4.0/

The consequence: phantom dependencies stop working. If your code imports a package you never installed — but which happened to get hoisted because some other library depends on it — npm resolves it fine and pnpm throws Cannot find module.

That failure is a feature. The import was always a bug; it just did not fail until the day the intermediate library dropped that dependency in a patch release and your production build broke for no visible reason.

What Changes in a TypeScript Project

Mostly nothing. pnpm add -D typescript and pnpm exec tsc behave exactly like the npm equivalents, and your tsconfig.json does not need a single pnpm-specific option.

The one area that needs attention is @types/* packages.

TypeScript picks up ambient type packages from node_modules/@types at the top level. Under npm's flat layout, a transitive @types/node lands there by accident and your process references type-check. Under pnpm, it does not — so if your code touches Node globals, you must install the types yourself:

[object Object]

With that in place, the usual things resolve:

const pnpmStorePath: string = process.env.PNPM_HOME ?? '~/.local/share/pnpm'
console.log(`store configured at ${pnpmStorePath}`)

Without it, the compiler has never heard of process. The rule is simple and worth adopting regardless of package manager: if you reference it, declare it. pnpm just enforces what npm let you get away with.

The same logic applies to libraries. A package that ships its own .d.ts files works unchanged. A JavaScript-only package needs its @types/* companion installed as a direct dev dependency, not inherited from somewhere up the tree.

Workspaces in a Monorepo

This is where pnpm earns its reputation. Workspaces are declared in a separate pnpm-workspace.yaml file rather than in package.json:

packages:
  - 'packages/*'
  - 'apps/*'

Internal packages reference each other with the workspace: protocol, which tells pnpm to link the local source instead of reaching for the registry:

{
  "name": "@acme/web",
  "dependencies": {
    "@acme/types": "workspace:*"
  }
}

On publish, pnpm rewrites workspace:* into the real version number, so the protocol never leaks into a published artifact.

For TypeScript specifically, the strict layout pays off twice over. Each package in the monorepo can only import what its own package.json declares, which means a shared types package cannot silently acquire a dependency on your web app's React version. Cross-package imports resolve through the symlink to real source files, so project references and tsc --build behave predictably.

A few commands you will use constantly:

pnpm -r build              # run "build" in every workspace package
pnpm --filter @acme/web dev   # run a script in one package
pnpm add -D typescript -w     # add to the workspace root

If install time or editor responsiveness in a large monorepo is your actual problem, the package manager is only one lever — the TypeScript performance guide covers the compiler side.

The Commands Worth Memorising

The mapping from npm is almost one to one:

npmpnpm
npm installpnpm install
npm install pkgpnpm add pkg
npm install -Dpnpm add -D pkg
npm uninstallpnpm remove
npm run buildpnpm build
npx tscpnpm dlx tsc

Two are worth calling out. pnpm exec runs a binary already installed in the project, while pnpm dlx fetches a package temporarily and runs it — that is the real npx equivalent. And pnpm why <package> prints the chain explaining why something is in your tree, which is the fastest way to find out who dragged in that duplicate version.

To pin the package manager itself so everyone on the team uses the same one, add a packageManager field to package.json:

{
  "packageManager": "pnpm@10.4.1"
}

Node's Corepack reads that field and uses the matching version, which ends the "works on my machine" lockfile churn that comes from two developers running different majors.

How It Compares to npm and Yarn

All three install the same packages from the same registry, so the choice is about layout, speed and strictness rather than what ends up in your bundle.

npm is the safe default. It ships with Node, every tool in the ecosystem assumes it works, and its flat node_modules never surprises a bundler. The cost is disk space and a tree that quietly lets undeclared imports resolve.

Yarn Berry (v2+) solved the same problems with Plug'n'Play, which drops node_modules entirely and resolves modules through a single lookup file. It is the more radical design and it is genuinely fast, but it needs editor and tooling support — for TypeScript that means the Yarn SDK, and anything that reads files off disk the old way has to be patched.

pnpm sits in between. You still get a real node_modules directory that tools can walk, so compatibility is close to npm's, but the layout is strict and the store is shared. For most TypeScript projects that is the better trade: the migration is short, and the thing you gain is a dependency graph you can trust.

Where pnpm Bites

Honest caveats, because they cost real time when you hit them cold:

Where to Go From Here

That is what pnpm is: the same packages, stored once and linked in, with a node_modules that reflects what you actually declared. The disk savings are nice and the install speed is nicer, but the strictness is the reason to switch — a dependency graph that lies to you is a build that breaks at the worst possible moment.

For a TypeScript project, the migration is usually pnpm import to convert the lockfile, pnpm install, then fixing whatever imports were phantom. After that it is business as usual: the tsconfig options still decide how strict the compiler is, the utility types are still where the language gets interesting, and the best-practices post collects the settings worth turning on once everything installs cleanly.

Share this article

Become a TypeScript Pro

Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges

Practice with Challenges

Put your what is pnpm? knowledge to the test with these related challenges.

#4Pick
Easy
#3312Parameters
Easy

Related Concepts

Concepts that build on or relate to what is pnpm?.

npm typescript: Install, Pin, Runtsconfig.json ExplainedWhat Is ESM? ES Modules ExplainedTypeScript PerformanceTypeScript Utility Types