The Dead Code Problem
Every TypeScript/React project accumulates dead code. Features get shipped and then deprecated. Components are replaced but the old version stays. CSS classes linger after refactors. Routes become unreachable after navigation changes. The average React project ships 15–30% dead code in its bundle, and the cost compounds:
- Bundle size. Dead exports that tree-shaking misses still ship to users. Every unused function, component, and CSS class adds kilobytes.
- Developer confusion. New developers waste hours understanding code that nothing references. Is it used? Is it safe to delete?
- Security surface area. Dead code can contain outdated dependencies with known vulnerabilities.
- Build time. The compiler still processes dead code. Type-checking, bundling, and linting all slow down.
The problem is that dead code is hard to find manually. An unused export might look perfectly valid — it's imported somewhere, or so you think. A CSS class might seem to be used until you realize the component that referenced it was deleted last month. Automated detection is the only scalable approach.
Tool 1: DeadCode — Multi-Category Dead Code Detection and Removal
DeadCode — Detect and Remove Unused Exports, Dead Routes, Orphaned CSS, and Unreferenced Components
scandetects 4 categories: unused exports, dead routes, orphaned CSS, unreferenced componentsremoveauto-removes dead code with--dry-runpreviewstatsquick overview of dead code in your project- Built for TypeScript/React/Next.js projects
DeadCode is a CLI tool that scans TypeScript/React/Next.js projects for four categories of dead code, then optionally removes it. It's designed for the workflow where you inherit a large codebase and need to find what's safe to delete.
Core workflow
# Install
pip install git+https://github.com/Coding-Dev-Tools/deadcode.git
# Scan for all dead code categories
deadcode scan
# ┌──────────────────────────────┬─────────────────────────────────────┐
# │ Category │ Example │
# ├──────────────────────────────┼─────────────────────────────────────┤
# │ unused_export │ export function oldHelper() — 0 │
# │ │ import sites │
# │ dead_route │ app/legacy/page.tsx — no internal │
# │ │ links from navigation │
# │ orphaned_css │ .oldClass in styles.module.css — 0 │
# │ │ JSX references │
# │ unreferenced_component │ <LegacyWidget> — defined but never │
# │ │ imported │
# └──────────────────────────────┴─────────────────────────────────────┘
# Quick stats overview
deadcode stats
# 47 dead items found across 4 categories
# 23 unused exports · 8 dead routes · 12 orphaned CSS · 4 unreferenced components
# Preview removal (no changes made)
deadcode remove --dry-run
# Auto-remove confirmed dead code
deadcode remove
# Filter by category
deadcode scan -c unused_export
deadcode scan -c dead_route
deadcode scan -c orphaned_css
# JSON output for CI/CD
deadcode scan --json-output
# Ignore generated code
deadcode scan -i "generated/" -i "**/*.generated.ts"
What DeadCode gets right
- Multi-category detection. Finds four types of dead code in one scan: unused exports, dead Next.js routes, orphaned CSS module classes, and unreferenced components. No other tool covers all four.
- Dead route detection. Specifically targets Next.js App Router projects. Finds page components that exist in the
app/directory but have no internal links pointing to them. knip and ts-prune don't detect dead routes. - Orphaned CSS detection. Finds CSS module classes that are defined in
.module.cssfiles but never referenced in JSX. This is a React-specific problem that generic TypeScript tools miss. - Auto-removal with dry-run.
deadcode remove --dry-runshows exactly what will be deleted.deadcode removeapplies the changes. No manual deletion needed. - Category filtering. Remove only one category at a time — start with the safest (orphaned CSS) before tackling unused exports.
- Monorepo support. Ignore patterns and include patterns work across monorepo structures.
Where DeadCode is limited
- TypeScript/React/Next.js only. Doesn't scan Vue, Svelte, or plain JavaScript projects. Won't detect unused Python or Go code.
- No dependency analysis. Doesn't find unused npm packages in
package.json. knip does this. - No type-level analysis. Uses AST pattern matching rather than TypeScript's type checker. May miss re-export chains that the compiler would resolve differently.
Tool 2: knip — Unused Exports, Files, and Dependencies
knip — Find Unused Exports, Files, Dependencies, and More
- Detects unused exports, types, dependencies, and files
- Uses TypeScript compiler API for precision
- Plugin system for frameworks (Next.js, React, etc.)
knip is an open-source tool that uses TypeScript's compiler API to find unused exports, unused types, unused dependencies, and unused files. It's more precise than regex-based tools because it resolves re-exports, namespace imports, and type-only imports through the compiler.
Core workflow
# Install
npm install -g knip
# Scan for unused exports and dependencies
knip
# ● 47 unused exports
# ● 3 unused dependencies (lodash, moment, classnames)
# ● 2 unused types
# ● 5 unused files
# Include unused dependencies
knip --dependencies
# Auto-fix (remove unused exports)
knip --fix
# CI mode
knip --no-exit-code # Report but don't fail
knip # Exit 1 if unused code found
What knip gets right
- Compiler-level precision. Uses TypeScript's type checker to resolve imports and re-exports. More accurate than regex-based scanning for complex export patterns.
- Unused dependencies. Finds npm packages in
package.jsonthat nothing imports. This catches dead weight in yournode_modules. - Unused files. Finds entire files that nothing imports or references. More aggressive than export-level detection.
- Framework plugins. Built-in plugins for Next.js, React, Nuxt, Svelte, Astro, and more. Understands framework-specific conventions (like Next.js page exports).
- Auto-fix.
knip --fixremoves unused exports in place. Not as safe as DeadCode's--dry-runapproach, but faster. - Open source. ISC license. Free, community-driven, actively maintained.
Where knip falls short
- No dead route detection. Doesn't detect unreachable Next.js routes. The Next.js plugin handles special exports, but it doesn't check whether any navigation link points to a route.
- No orphaned CSS detection. Doesn't scan CSS module files for unreferenced classes. This is a significant gap for React projects that use CSS modules heavily.
- No category filtering. You can't run "only unused exports" or "only orphaned CSS" — knip reports everything it finds in one pass.
- Node.js dependency. Requires Node.js runtime. Teams using Python toolchains need an additional runtime.
- Slower on large projects. TypeScript compiler analysis is more precise but also more expensive. Large monorepos can take 30–60 seconds.
Best for: Teams that need unused dependency detection and compiler-level precision for TypeScript export analysis. Supplement with DeadCode for dead routes and orphaned CSS.
Tool 3: ts-prune — Unused Export Detection
ts-prune — Find Unused Exports in TypeScript Projects
- Detects exported symbols with zero import sites
- Uses TypeScript compiler API
- Minimal, focused scope
ts-prune is the original unused export detector for TypeScript. It uses the compiler API to find symbols that are exported but never imported — the simplest and most common form of dead code. It's minimal, fast, and focused on one job.
Core workflow
# Install
npm install -g ts-prune
# Find unused exports
ts-prune
# src/utils/helpers.ts:12 — export function oldHelper (unused)
# src/types/api.ts:45 — export interface OldResponse (unused)
# src/components/LegacyWidget.tsx:8 — default export LegacyWidget (unused)
# Filter out commonly used symbols
ts-prune | grep -v "used in module"
# CI mode — exit 1 if unused exports found
ts-prune && echo "Clean" || echo "Unused exports found"
What ts-prune gets right
- Minimal and fast. Does one thing well — finds unused exports. No configuration needed. Scans most projects in under 10 seconds.
- Compiler-level precision. Uses TypeScript's compiler API. Handles re-exports, namespace imports, and type-only imports correctly.
- Simple output. File:line — export name. Easy to grep, pipe, or process.
- Open source. MIT license. Free and lightweight.
Where ts-prune falls short
- Export-only detection. Only finds unused exports. No dead routes, no orphaned CSS, no unreferenced components, no unused dependencies. knip is strictly more capable.
- No auto-removal. Reports what's unused but doesn't delete it. You remove dead code manually or write your own script.
- No framework awareness. Doesn't understand Next.js conventions, React patterns, or any framework-specific export semantics. May flag false positives on Next.js page exports, route handlers, or middleware.
- Maintenance status. Less actively maintained than knip. New TypeScript features may not be supported immediately.
- No category filtering. All unused exports in one list. Can't filter by type (function, class, interface, etc.).
Best for: Quick one-off scans for unused exports. If you need anything beyond export detection — dead routes, orphaned CSS, auto-removal, dependency analysis — use DeadCode or knip instead.
Tool 4: ESLint no-unused-vars — Local Variable Detection
ESLint no-unused-vars — Find Unused Local Variables and Parameters
- Detects unused local variables, function parameters, and imports
- Already in most TypeScript projects
- Customizable via ESLint config
ESLint's no-unused-vars rule is probably already running in your project. It catches unused local variables, function parameters, and top-level imports within a single file. It's the most basic form of dead code detection, but it has a critical limitation: it only looks within a single file.
Core workflow
# Already configured in most projects
# .eslintrc.json
{
"rules": {
"no-unused-vars": "warn",
"@typescript-eslint/no-unused-vars": ["warn", {
"vars": "all",
"args": "after-used",
"ignoreRestSiblings": true
}]
}
}
# Run ESLint
npx eslint src/
# Output:
# src/utils.ts
# 12:10 warning 'oldHelper' is defined but never used no-unused-vars
# src/api.ts
# 45:7 warning 'oldResponse' is defined but never used no-unused-vars
What ESLint no-unused-vars gets right
- Already installed. If you use ESLint (and you should), this rule is either already enabled or trivially added. No new dependency.
- Zero cost. Runs as part of your existing lint step. No additional CI time.
- Local variable detection. Catches unused local variables that export-level tools (DeadCode, knip, ts-prune) miss. An unused
constinside a function is invisible to export scanners. - Customizable. Configure which variables to check, ignore underscore-prefixed names, allow unused function parameters after used ones, etc.
Where ESLint no-unused-vars falls short for dead code detection
- File-scoped only. Can't detect unused exports. An exported function that nothing imports looks "used" to ESLint because it's referenced in the
exportstatement. This is the biggest gap — most dead code in real projects is unused exports, not unused locals. - No dead route detection. Doesn't understand Next.js routing or any framework conventions.
- No orphaned CSS detection. Doesn't scan CSS files.
- No auto-removal. Reports warnings but doesn't delete code.
--fixonly works for simple cases. - Not designed for dead code.
no-unused-varsis a lint rule, not a dead code detection tool. It catches a subset of dead code as a side effect of linting.
Best for: Every project. It's already in your lint config. But it only catches local unused variables — not unused exports, dead routes, or orphaned CSS. Use alongside DeadCode for full coverage.
Feature Comparison
| Capability | DeadCode | knip | ts-prune | ESLint no-unused-vars |
|---|---|---|---|---|
| Unused export detection | ✓ | ✓ | ✓ | ✗ |
| Dead route detection (Next.js) | ✓ | ✗ | ✗ | ✗ |
| Orphaned CSS detection | ✓ | ✗ | ✗ | ✗ |
| Unreferenced component detection | ✓ | ~ Via unused exports | ~ Via unused exports | ✗ |
| Unused dependency detection | ✗ | ✓ | ✗ | ✗ |
| Unused file detection | ✗ | ✓ | ✗ | ✗ |
| Unused local variable detection | ✗ | ✗ | ✗ | ✓ |
| Auto-removal with dry-run | ✓ | ~ --fix (no dry-run) | ✗ | ~ --fix (limited) |
| Category filtering | ✓ -c flag | ✗ | ✗ | ✓ Rule config |
| Framework awareness | ✓ Next.js/React | ✓ Plugins | ✗ | ✗ |
| JSON output for CI | ✓ | ✓ | ✗ | ✓ (ESLint JSON) |
| Runtime | Python | Node.js | Node.js | Node.js |
| Open source | ✓ MIT | ✓ ISC | ✓ MIT | ✓ MIT |
Detection Scope Compared
The tools cover different parts of the dead code problem:
| Dead Code Type | DeadCode | knip | ts-prune | ESLint no-unused-vars |
|---|---|---|---|---|
| Unused export (function) | ✓ | ✓ | ✓ | ✗ |
| Unused export (type/interface) | ✓ | ✓ | ✓ | ✗ |
| Unused Next.js route | ✓ | ✗ | ✗ | ✗ |
| Orphaned CSS class | ✓ | ✗ | ✗ | ✗ |
| Unreferenced component | ✓ Named category | ~ As unused export | ~ As unused export | ✗ |
| Unused npm package | ✗ | ✓ | ✗ | ✗ |
| Unused file | ✗ | ✓ | ✗ | ✗ |
| Unused local variable | ✗ | ✗ | ✗ | ✓ |
CI/CD Integration Compared
# DeadCode — gate on any dead code category
deadcode scan --json-output
# Exit 0 = clean, exit 1 = dead code found
# knip — gate on unused exports and dependencies
knip
# Exit 0 = clean, exit 1 = unused code found
# ts-prune — gate on unused exports only
ts-prune
# Exits with 0 always; pipe to grep for CI gating
# ESLint — gate on unused local variables
npx eslint src/ --max-warnings 0
# Exit 0 = clean, exit 1 = lint warnings found
Recommended CI/CD pipeline
name: Dead Code Check
on: [push, pull_request]
jobs:
dead-code:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Layer 1: ESLint (local unused vars) — fastest, already installed
- run: npx eslint src/ --max-warnings 0
# Layer 2: DeadCode (exports, routes, CSS) — React/Next.js specific
- run: pip install git+https://github.com/Coding-Dev-Tools/deadcode.git
- run: deadcode scan
# Layer 3: knip (unused dependencies) — catch npm bloat
- run: npx knip --dependencies
When to Use Which
Use DeadCode when:
You have a React or Next.js project and need to find unused exports, dead routes, orphaned CSS, and unreferenced components in one scan. You want category-specific filtering and safe auto-removal with dry-run preview. This is the most complete dead code detector for React/Next.js projects.
Use knip when:
You need compiler-level precision for TypeScript export analysis and want to find unused npm dependencies and unused files. Best used alongside DeadCode — knip for dependencies and compiler-verified exports, DeadCode for routes, CSS, and React-specific patterns.
Use ts-prune when:
You need a quick, minimal check for unused exports and nothing else. ts-prune is simpler than knip but strictly less capable. Consider upgrading to knip or DeadCode for any serious dead code cleanup.
Use ESLint no-unused-vars when:
You already have ESLint configured (which you should). It catches local unused variables that the other tools miss. Not sufficient on its own — pair with DeadCode for export-level detection and React/Next.js-specific dead code.
The Complementary Stack
No single tool catches all dead code. The most effective approach layers them:
# 1. ESLint no-unused-vars — local unused variables (already in your lint step)
npx eslint src/ --max-warnings 0
# 2. DeadCode — React/Next.js specific dead code
deadcode scan
deadcode remove --dry-run # Preview first
# 3. knip — unused dependencies and compiler-verified exports
knip --dependencies
# What each layer catches that the others miss:
# - ESLint: unused const/let inside functions
# - DeadCode: dead Next.js routes, orphaned CSS classes
# - knip: unused npm packages in package.json
Together these three layers cover every major category of dead code in a TypeScript/React project. DeadCode handles the React-specific categories (routes, CSS, components), knip handles dependency bloat, and ESLint handles local variables.
Cost Comparison
| Cost Factor | DeadCode | knip | ts-prune | ESLint no-unused-vars |
|---|---|---|---|---|
| License | MIT (free tier available) | ISC (free) | MIT (free) | MIT (free) |
| Paid features | $15/mo unlimited projects | Free | Free | Free |
| Full suite cost (11 tools) | $49/mo Suite | N/A | N/A | N/A |
Install DeadCode
# Install via pip
pip install git+https://github.com/Coding-Dev-Tools/deadcode.git
# Or via Homebrew (macOS/Linux)
brew tap Coding-Dev-Tools/tap
brew install deadcode
# Or via Scoop (Windows)
scoop bucket add Coding-Dev-Tools https://github.com/Coding-Dev-Tools/scoop-bucket
scoop install deadcode
# Scan your project
deadcode scan
Star DeadCode on GitHub
Related Reading
- Clean Up Your React/Next.js Codebase with DeadCode — practical tutorial
- Fail CI on Dead Code: Automate React Codebase Hygiene — CI/CD integration
- DeadCode Under the Hood: How Static Analysis Finds Dead React/Next.js Code — technical deep dive
- Config Drift Detection Compared — ConfigDrift vs driftctl vs Terraform Plan vs Checkov