Dead-Code Cleanup: Knip Expands as Older Detection Tools Show Their Age

Developers looking for unused code face a changed tooling landscape: depcheck is archived, and ts-prune's npm release remains unchanged since 2021. Knip offers a broader alternative, but removing files, exports and dependencies still requires checking what automated reports can miss.

Kevin Osei
lake-3655959b-a7e3-4e15-843a-64095b9b0fa7-0.webp

Choosing a detector now means checking its maintenance

Knip scans for unused files, exports, types and dependencies across projects, including monorepos and workspaces. Its configuration file, knip.json, defines entry points, project files and intentional exclusions. The project reports 86 million monthly downloads, more than 21,000 public projects using it and over 190 plugins covering tools such as Next.js, Vite, Vitest, Jest, Storybook and Nx. Version 6.43.0 was released on October 24, 2026.

Two narrower tools remain relevant to understanding existing cleanup workflows. Depcheck checks source imports against package.json, identifying both unused packages and imported packages missing from the manifest. Its maintainers archived the repository on June 16, 2025, leaving it read-only. The README says development is no longer active and recommends Knip for stronger support for TypeScript, monorepos and modern build tools.

Ts-prune uses the TypeScript compiler API to identify exports with no imports elsewhere in a project. According to repowise's comparison, its latest npm release as of late October 2026 was still version 0.10.3, published in December 2021. That release includes TypeScript 4.5, which predates the moduleResolution: bundler option introduced in TypeScript 5.0. A modernization commit reached the GitHub repository in September 2026, but running npx ts-prune still installs the older npm code.

What the tools are looking for

Code can remain in a repository, pass type checks and tests, and contribute nothing to the running application. Detecting it requires more than one kind of analysis. Statements after an unconditional return, throw or break, or behind an infinite loop, are unreachable. A private function, variable, class or import can instead be reachable in principle but have no references within its module.

Exports create a different problem: a function or component may look like a valid public interface even though nothing in the codebase imports it. Redundant logic can also survive a refactor, such as a switch branch whose case is already handled earlier. Feature flags leave another category of leftovers when a permanently enabled or disabled flag freezes one side of a conditional.

Refactoring is the most common source described in the donor guide: developers remove a call site but leave its former implementation. Other routes include retaining the unused parts of copied or vendored utilities, leaving framework starter files in place, merging abandoned experiments and indefinitely postponing the removal of deprecated APIs. Removing a flag from configuration without removing its conditional logic creates the same problem.

The costs extend beyond bundle size

Several studies and reports summarized by Axify illustrate the scale, though they cover different languages and workloads. An investigation spanning 35 open-source Java projects found that an average of about 15.94% of methods were effectively dead; 32 of the 35 applications contained dead methods. The 2021 Muzeel study examined around 40,000 web pages and found that half had at least 70% of their JavaScript functions unused. Removing unused functions reduced payloads by as much as 60%.

Axify also cites Azul Systems' finding that nearly 20% of code in larger applications was unused, with some cases reaching 66%. That excess slows build and test pipelines and extends the wait for feedback. TypeScript still has to parse and type-check dead files, while package installation still has to resolve unused dependencies. A discarded module that imports a 200 KB charting library can even double an application's initial bundle.

Unused dependencies also remain subject to security scanning and license obligations. They can generate CVE alerts despite serving no useful application function. Meanwhile, tests for obsolete code continue consuming CI time and can create misleading confidence in coverage. Developers reading the repository must spend time understanding branches and functions that no longer matter.

Feature-flag cleanup deserves particular attention. A 2021 industry report cited by Axify found that 73% of flags remained after their purpose had ended. On August 1, 2012, Knight Capital lost $440 million in roughly 45 minutes after a flag dormant for eight years reactivated an old code branch. The incident illustrates the risk of leaving obsolete behavior available for reactivation, rather than establishing that every unused branch poses the same danger.

Large-scale removal has produced measurable savings. Meta Platforms' SCARF initiative deleted more than 104 million lines of code and petabytes of unused data, saving over one megawatt of compute power, according to Axify.

Use different checks for different gaps

Local checks can begin with TypeScript's noUnusedLocals and noUnusedParameters options. They flag unread locals, unused private functions and unreferenced parameters. ESLint's @typescript-eslint/no-unused-vars rule provides similar checks with additional configuration, including allowances for underscore-prefixed arguments. These checks do not resolve the broader question of whether exported symbols have consumers elsewhere.

Knip can supply that project-wide view and can also apply changes. With knip --fix, it can strip the export keyword from unused exports and types, remove unused enum members and remove unused dependencies. Deleting unused files requires the additional --allow-remove-files flag.

Madge examines the module dependency graph for orphaned modules and circular dependencies. Running madge --circular src/ identifies cycles that can obstruct tree-shaking and cause undefined values during initialization.

Coverage tools answer a separate question: what did the tests execute? C8, nyc and Vitest's coverage provider expose untested lines and branches. An uncovered path may warrant investigation, but lack of test execution alone does not establish that production never uses it.

Bundlers provide another layer of removal. Rollup, esbuild and webpack 5+ can eliminate unused ES-module exports, while minifiers such as terser and swc can discard unreachable branches. Static analysis has limits, however: dynamic imports, reflection and modules with side effects can resist elimination. Rollup's moduleSideEffects: false setting allows more aggressive removal, but polyfills, CSS imports and global registrations must be identified as side-effectful to avoid deleting required behavior.

Review findings, remove code and rerun the checks

A cleanup workflow can start with the TypeScript checks, followed by a configured Knip scan and review of its findings. Madge can then expose cycles that need to be broken. Coverage thresholds help enforce testing expectations for new code, while CI scripts such as lint:dead and check:cycles can make detected regressions fail the build.

Depcheck can provide a dependency-specific second opinion, particularly for missing packages, but its archived status belongs in that decision. Its own maintainers now recommend switching to Knip rather than treating depcheck as an actively maintained companion.

The guide's worked TypeScript example shows how the checks complement one another. Knip reported an unused experimental file, the unused packages lodash and moment, and three orphaned exports: formatBytes, formatCurrency and Modal. Depcheck agreed on the two unused packages and also found that zod was imported without being listed in package.json. Madge identified a cycle linking index.ts and api/client.ts.

The example's cleanup removed the experimental file and orphaned exports, uninstalled lodash and moment, installed zod and broke the cycle. Extracting a shared type or moving an import can resolve that kind of circular dependency. The workflow then reran all three tools to check the result.

Keep intentional code out of the deletion queue

A report of no internal consumers is not enough to justify removing a published library export. That export remains part of the public API; deleting it is a breaking change to schedule for a major release and document in the changelog. Knip's entry-point and ignore settings can distinguish the public contract from genuinely unused internal code.

Other exclusions need explicit reasons. A temporarily disabled feature flag intended to return is dormant, not obsolete, and should be tracked. Generated clients, schema types and build artifacts should be excluded rather than edited by hand. Defensive branches can preserve graceful failure for supposedly impossible inputs and should carry a comment explaining why they remain. Code kept solely to demonstrate an API pattern should likewise be an intentional exception.

Dynamic imports, tests and plugin systems can all make a tool's unused-export finding misleading. Review those paths before deleting a symbol, then run the full test suite after each removal and check coverage for regressions.

← Back to Technology

Related news