Finding Unused Dependencies and Exports with Knip
Every dependency you declare is something to install, audit, update and trust, and in a mature codebase a surprising share of them are no longer used. Unused exports and files accumulate the same way. Knip analyses a project — or every package in a monorepo — and reports unused dependencies, undeclared ("unlisted") dependencies, unused files, unused exports and unused types, using plugins that understand the configuration files of more than a hundred tools. This guide sets Knip up for a workspace, explains how to read and trust its report, and turns it into a CI gate that stays green.
Why dependency hygiene matters
Unused dependencies are not harmless clutter. Each one adds install time and disk usage, appears in npm audit results you then have to triage, receives update pull requests from your bots, and is one more package that can be compromised in a supply-chain attack. The reverse problem — imports of packages that are not declared — produces the phantom dependency failures that break installs under strict package managers. The wider auditing workflow is covered in Dependency Auditing and Automated Updates.
A typical first Knip run on an older repository looks like this:
$ npx knip
Unused files (6)
src/legacy/formatters.ts
src/utils/old-date.ts
Unused dependencies (4)
moment packages/api/package.json
lodash.merge apps/web/package.json
Unused devDependencies (3)
@types/jest packages/ui/package.json
Unlisted dependencies (2)
qs packages/api/src/http.ts
date-fns apps/web/src/lib/time.ts
Unused exports (11)
formatCurrency function packages/utils/src/money.ts:14:17
How Knip works
Knip starts from entry files — your main and exports targets, test files, scripts referenced in package.json, and files that tools load by convention, such as vite.config.ts or .eslintrc.cjs. It builds the module graph from those entries, then compares what it found with what you declared. Anything declared but never imported is unused; anything imported but not declared is unlisted; any file not reachable from an entry is unused.
Plugins are what make Knip practical. A dependency like @vitejs/plugin-react is never imported by application code; it is imported by vite.config.ts. Knip's Vite plugin knows to treat that config as an entry, so the dependency counts as used. Plugins activate automatically when the corresponding tool is a dependency.
Setting it up in a workspace
npm install -D knip
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"workspaces": {
".": {
"entry": ["scripts/*.ts"],
"ignoreDependencies": ["@acme/tsconfig"]
},
"apps/*": {
"entry": ["src/main.tsx", "src/routes/**/*.tsx"],
"project": ["src/**/*.{ts,tsx}"]
},
"packages/*": {
"project": ["src/**/*.ts"]
}
},
"ignoreExportsUsedInFile": true
}
Save it as knip.json at the repository root. Knip reads each workspace's package.json and checks each package against its own dependencies, so a dependency declared in apps/web but used only in packages/ui is correctly reported as both unused in one and unlisted in the other.
entryadds entry files Knip cannot infer, such as route files loaded by a file-system router or scripts invoked by CI.projectlimits which files count as part of the project, so generated or vendored files do not show up as unused.ignoreDependenciessuppresses known false positives — for example, a shared configuration package referenced only fromtsconfig.jsonextends(which Knip usually understands, but not in every setup).ignoreExportsUsedInFileavoids reporting exports that are used within the same file.
Reading the report and avoiding false positives
Treat the first report as a list of questions, not a list of deletions. Common false-positive sources:
| Report | Usual cause | Fix |
|---|---|---|
| Unused dependency used via CLI in a script | Binary used from scripts but Knip missed it |
Check the script; add to entry or ignoreBinaries |
| Unused file loaded dynamically | import() with a computed path |
Add the pattern to entry |
| Unused export used by another repository | Public API of a published package | Knip treats exports entries as public; check the map |
| Unused dependency that is a peer | Required by a plugin at runtime | Declare it; add to ignoreDependencies with a comment |
Unused exports deserve extra care in libraries. An export that nothing inside the repository imports may still be public API used by consumers. Knip considers exports reachable from the package's exports map to be used, so internal modules' unused exports are reported while your public surface is not. If you see a public function reported, check that the exports map actually exposes the module it lives in.
Worked example: a first cleanup pass
A team runs Knip on a twelve-package repository for the first time and gets 140 findings. They split the work: dependencies first, because removing them has the largest effect on install and audit noise. Of 31 unused dependencies, 26 are genuinely dead (including moment, replaced by date-fns two years earlier but never removed), three are false positives from a custom build script Knip could not see (fixed with an entry pattern), and two are peers required by an ESLint plugin (declared and ignored with comments). The eight unlisted dependencies are declared in the right packages. Unused files go next — 19 legacy modules deleted. Unused exports are left for a later pass, since removing them from library packages needs a changelog decision. The cleanup removes 180 packages from the lockfile and shortens npm audit output by a third.
Unused exports and types in library packages
Unused exports are the most numerous and the least urgent finding, but in a monorepo they carry real cost. An exported function nobody imports is still compiled, type-checked, documented and maintained; it is also API surface that someone may start depending on tomorrow. For internal packages — those marked "private": true and consumed only inside the repository — Knip's unused-export report is trustworthy: if nothing in the repository imports it, nothing anywhere does. Remove those exports freely.
For published packages, split the work. Exports reachable through the exports map are public and Knip treats them as used. Exports that are unused and not reachable from the map are internal helpers that happen to be exported, usually for tests; mark them with a JSDoc @internal tag, which Knip can be configured to accept, or stop exporting them and test through the public API.
Types need a separate decision. Knip reports unused exported types and interfaces, and in libraries those are often intentionally exported so consumers can annotate their own code — ClientOptions, ButtonProps. Configure "ignoreExportsUsedInFile" and, if needed, the types category's rules so that types used in public signatures are not flagged, and review the remainder by hand.
Keeping the analysis fast
On large repositories, a full Knip run can take a minute or more because it builds a complete module graph. Three settings keep it quick enough for every pull request. Limit project globs to source folders so generated code, fixtures and build output are never parsed. Use --workspace <name> in local development to analyse one package and its dependencies. And cache the run: Knip supports --cache, which stores parsed module information between runs and makes repeat runs in CI significantly faster when combined with a cache step keyed on the lockfile.
CI integration
Once the report is clean, keep it clean. Run Knip in CI with only the categories you have fixed, and expand as you go:
- run: pnpm install --frozen-lockfile
- name: Dependency hygiene
run: pnpm exec knip --include dependencies,unlisted,files
Knip exits non-zero when it finds issues, so the job fails on any new unused dependency or unlisted import. For large repositories, --production mode analyses only production entry points and dependencies (excluding tests and dev tooling), which is useful as a separate, stricter check on what ships.
Prevention and CI/CD guardrails
- Make Knip a required check for the categories you have cleaned up.
- Document every ignore in
knip.jsonwith a comment-style note or a linked issue. - Run it on dependency-bot pull requests too, so an update that removes a transitive need is visible.
- Pair it with
import/no-extraneous-dependenciesin ESLint for editor-time feedback on unlisted imports.
Frequently Asked Questions
How is Knip different from depcheck? depcheck focuses on dependencies and has fewer tool integrations. Knip also finds unused files and exports, understands workspaces natively and has a large plugin ecosystem, which makes its dependency results more accurate in modern setups.
Does Knip support monorepos with pnpm, Yarn and npm workspaces? Yes. It reads the workspace configuration of each package manager and analyses each package against its own manifest.
Is it safe to delete everything Knip reports? Not blindly. Dynamic imports, framework conventions and public APIs can produce false positives. Review each category, add entries or ignores where needed, and run the full test suite after removals.
Can Knip fix issues automatically?
knip --fix removes unused dependencies from manifests and unused exports from source files. Use it on a branch and review the diff; it is best for the dependency category, where changes are mechanical.
Related
- Dependency Auditing and Automated Updates covers the wider dependency hygiene workflow.
- Fixing Phantom Dependencies After Switching to pnpm explains the unlisted-dependency failures Knip detects.
- Keeping Workspace Dependency Versions in Sync with syncpack handles the related problem of version drift.
- Detecting Undeclared Cross-Package Imports applies the same checks to imports between workspace packages.