← All articles
CODE QUALITY Knip: Finding Dead Code and Unused Dependencies in J... 2026-02-15 · 7 min read · knip · dead-code · unused-dependencies

Knip: Finding Dead Code and Unused Dependencies in JavaScript Projects

Code Quality 2026-02-15 · 7 min read knip dead-code unused-dependencies static-analysis typescript javascript code-quality linting

Knip: Finding Dead Code and Unused Dependencies in JavaScript Projects

Knip dead code detection logo

Every codebase accumulates dead code. A utility function written for a feature that got reverted. A dependency added for one component that nobody uses anymore. An exported type that no consumer imports. A configuration file for a tool the team stopped using two years ago. Individually, each piece of dead code is harmless. Collectively, they slow down builds, confuse new developers, inflate bundle sizes, and create a false sense of complexity.

Knip is a static analysis tool that finds all of it: unused files, unused exports, unused dependencies, unlisted dependencies, and unused configuration. It understands the JavaScript and TypeScript ecosystem deeply -- it knows how tools like Jest, Storybook, Vitest, ESLint, Webpack, and dozens of others reference files, so it doesn't flag your test setup file as "unused" just because nothing imports it directly.

What Knip Detects

Knip performs several categories of analysis in a single run:

This breadth is what distinguishes Knip from simpler tools. ESLint can flag unused variables within a file, but it can't tell you that an entire file is unused. depcheck can find unused dependencies, but it doesn't understand unused exports. Knip does all of it in a single pass.

Quick Start

Installation

# bun
bun add -D knip

# npm
npm install -D knip

# pnpm
pnpm add -D knip

First Run

bunx knip

That's it. With zero configuration, Knip analyzes your project using sensible defaults: it finds your entry points (typically src/index.ts or whatever package.json main/exports point to), traces all imports, and reports everything that's unreachable.

Example Output

Unused files (3)
  src/utils/deprecated-helpers.ts
  src/components/OldNavbar.tsx
  src/lib/legacy-api-client.ts

Unused dependencies (2)
  moment
  lodash

Unused devDependencies (1)
  @types/lodash

Unused exports (5)
  src/utils/format.ts: formatPhoneNumber
  src/utils/format.ts: formatSSN
  src/hooks/useDebounce.ts: useDebounce (type)
  src/api/client.ts: ApiClient (class)
  src/types/index.ts: LegacyUser (type)

Unlisted dependencies (1)
  zod (imported in src/api/validation.ts but not in package.json)

Each finding is actionable. Unused files can be deleted. Unused dependencies can be removed from package.json. Unused exports can be unexported (or the function deleted if it's truly dead). Unlisted dependencies need to be added to package.json.

Configuration

Knip works without configuration for simple projects, but real codebases need to tell Knip about non-standard entry points and tool-specific files. Create a knip.json (or knip.ts for TypeScript) in your project root:

{
  "$schema": "https://unpkg.com/knip@latest/schema.json",
  "entry": [
    "src/index.ts",
    "src/server.ts",
    "scripts/*.ts"
  ],
  "project": ["src/**/*.{ts,tsx}"],
  "ignore": [
    "src/**/*.test.ts",
    "src/**/*.stories.tsx"
  ],
  "ignoreDependencies": [
    "autoprefixer"
  ]
}

Entry Points

Entry points are the roots of your dependency graph. Knip traces imports starting from these files. Anything not reachable from an entry point is flagged as unused.

{
  "entry": [
    "src/index.ts",
    "src/worker.ts",
    "src/cli.ts",
    "scripts/**/*.ts"
  ]
}

Knip automatically detects entry points for many tools (Next.js pages, Remix routes, Vitest test files, etc.), so you often only need to add custom entry points that aren't covered by plugin auto-detection.

Plugin System

Knip's real power is its plugin system. It has built-in plugins for 80+ tools that understand how each tool references files. This means Knip won't incorrectly flag your jest.setup.ts as unused -- it knows Jest references it through jest.config.ts.

Supported tools include:

Category Plugins
Frameworks Next.js, Remix, Astro, Nuxt, SvelteKit, Gatsby
Testing Vitest, Jest, Playwright, Cypress, Storybook
Bundlers Webpack, Vite, Rspack, Rollup, esbuild
Linting ESLint, Biome, Prettier, Stylelint, Commitlint
Build TypeScript, Babel, SWC, PostCSS, Tailwind CSS
CI/CD GitHub Actions, Husky, lint-staged
Other Drizzle, Prisma, GraphQL Codegen, Sentry, Knex

Plugins are enabled automatically when Knip detects the tool's configuration file in your project. You can explicitly configure them if the defaults aren't right:

{
  "vitest": {
    "config": ["vitest.config.ts"],
    "entry": ["src/**/*.{test,spec}.ts"]
  },
  "storybook": {
    "config": [".storybook/main.ts"],
    "entry": ["src/**/*.stories.tsx"]
  }
}

Monorepo Support

Knip handles monorepos natively. Define workspace-specific configuration:

{
  "workspaces": {
    "packages/core": {
      "entry": ["src/index.ts"],
      "ignoreDependencies": ["some-peer-dep"]
    },
    "packages/ui": {
      "entry": ["src/index.ts", "src/components/*/index.ts"]
    },
    "apps/web": {
      "entry": ["src/main.tsx"]
    }
  }
}

Knip traces cross-workspace imports correctly. If apps/web imports from packages/core, those exports are marked as used.

Comparison with Alternatives

Feature Knip eslint-plugin-unused-imports depcheck ts-prune unimported
Unused files Yes No No No Yes
Unused exports Yes No No Yes No
Unused dependencies Yes No Yes No No
Unlisted dependencies Yes No Yes No No
Unused devDependencies Yes No Partial No No
Tool-aware (80+ plugins) Yes No Limited No Limited
Monorepo support Yes N/A No No No
Auto-fix Partial Yes (remove imports) No No No
Performance (large project) Fast Fast Slow Moderate Moderate
Configuration needed Minimal ESLint config Minimal tsconfig Moderate

Knip vs eslint-plugin-unused-imports

eslint-plugin-unused-imports is useful but solves a much narrower problem. It finds unused import statements within individual files and can auto-fix them (remove the import line). It doesn't know whether the imported module itself is unused by the rest of the project, whether the dependency is needed in package.json, or whether entire files are dead.

Think of it this way: eslint-plugin-unused-imports cleans up within files. Knip cleans up across your entire project. They're complementary -- use both.

Knip vs depcheck

depcheck specifically targets package.json accuracy: are all listed dependencies actually used? Are all used dependencies listed? It does this job reasonably well but has significant blind spots:

Knip replaces depcheck entirely and adds much more.

Knip vs ts-prune

ts-prune finds unused exports in TypeScript projects. It was useful when it was the only option, but Knip subsumes its functionality and adds unused file detection, dependency analysis, and plugin-aware scanning. ts-prune is also unmaintained as of 2024.

CI Integration

Running Knip in CI catches dead code before it merges. Add it to your pipeline:

{
  "scripts": {
    "knip": "knip",
    "knip:ci": "knip --no-progress"
  }
}

GitHub Actions

- name: Check for unused code
  run: bunx knip --no-progress

Knip exits with a non-zero code when it finds issues, so your CI pipeline will fail on unused code. This is the single most effective way to prevent dead code accumulation.

Strict vs. Gradual Adoption

For existing projects with years of accumulated dead code, failing CI immediately isn't practical. Use Knip's --include flag to enable categories gradually:

# Start with just unused dependencies (easiest to fix)
bunx knip --include dependencies

# Add unlisted dependencies
bunx knip --include dependencies,unlisted

# Add unused files
bunx knip --include dependencies,unlisted,files

# Eventually, enable everything (the default)
bunx knip

This lets you clean up one category at a time, merge the cleanups, and then tighten the CI check.

Reporter Options

Knip supports multiple output formats:

# Default human-readable output
bunx knip

# JSON for programmatic consumption
bunx knip --reporter json

# Compact (one line per issue)
bunx knip --reporter compact

# Markdown (useful for PR comments)
bunx knip --reporter markdown

Practical Workflow

Here's how to use Knip effectively on an existing project:

Step 1: Baseline Run

bunx knip | tee knip-baseline.txt

Review the output. You'll likely see dozens or hundreds of findings on a mature codebase. Don't panic.

Step 2: Fix False Positives

Some findings will be incorrect -- files or exports that are used in ways Knip can't detect (dynamic imports, metaprogramming, external tool references). Update your configuration:

{
  "ignore": [
    "src/generated/**"
  ],
  "ignoreDependencies": [
    "autoprefixer",
    "@babel/runtime"
  ],
  "ignoreExportsUsedInFile": true
}

The ignoreExportsUsedInFile option is particularly useful: it tells Knip not to flag exports that are used within the same file (common for barrel files that re-export internal utilities).

Step 3: Clean Up Genuine Dead Code

Work through the remaining findings:

Step 4: Add to CI

Once the baseline is clean, add Knip to your CI pipeline so dead code can't creep back in.

Step 5: Run Periodically with --strict

The --strict flag enables additional checks beyond the default:

bunx knip --strict

This is more aggressive about flagging potential issues and is useful for periodic deep-cleaning, even if it's too noisy for CI.

Performance

Knip is fast because it leverages TypeScript's compiler API for import resolution rather than doing its own parsing. On a project with 2,000 TypeScript files:

This is fast enough to run on every CI build and as a pre-commit hook for smaller projects.

Common Pitfalls

Dynamic imports: import(variable) can't be statically analyzed. If you dynamically import modules, add those modules as entry points in Knip's configuration.

Barrel files: A file like src/utils/index.ts that re-exports everything from submodules might show unused exports if some re-exported items aren't consumed externally. Use ignoreExportsUsedInFile or restructure your barrel files.

Types-only packages: Some @types/* packages are used by TypeScript's type resolution but not explicitly imported. Knip usually detects these correctly through its TypeScript plugin, but occasionally you'll need to add them to ignoreDependencies.

Peer dependencies: Packages listed as peer dependencies may appear unused because the host package provides them. Configure ignoreDependencies for legitimate peer deps.

The Bottom Line

Dead code is a maintenance tax that compounds over time. Every unused file is a file that new developers read and try to understand. Every unused dependency is an attack surface and a bun install slowdown. Every unused export is a signal that the codebase might use something it actually doesn't.

Knip finds all of it with minimal configuration and integrates cleanly into CI pipelines. The initial cleanup of an existing codebase takes a few hours. After that, keeping the codebase clean is automatic. Of all the code quality tools available for JavaScript and TypeScript projects, Knip has one of the highest return-on-investment ratios: install it, run it, delete what it finds, and your codebase is immediately smaller, faster, and easier to understand.