Short answer: tsc and ESLint only find unused code inside one file: local variables, parameters and imports. To find unused exports, classes and whole files, you need a tool that reads the whole project, such as knip, ts-prune or Skylos.

We built a small TypeScript project with nine pieces of dead code and ran the TypeScript compiler and Skylos on it. Here's what each one found.

The test project

src/
  index.ts            entry point: creates a Cart and prints the total
  cart.ts             Cart (used), OldCart (never used)
  utils/index.ts      export * from "./format"
  utils/format.ts     formatPrice (used), formatPriceLegacy, LEGACY_TAX_RATE, LegacyOrder (never used)
  legacy/report.ts    buildReport: nothing imports this file

cart.ts also has an unused import, an unused local variable and a public method, exportCsv, that nothing calls. formatPrice is only reached through the utils/index.ts barrel, a common pattern that whole-project tools can get wrong.

Results

Planted dead codetsc --noUnusedLocalsSkylos 4.48.0
Unused import (readFileSync)FoundFound
Unused local variable (unusedCount)FoundMissed
Unused exported function (formatPriceLegacy)MissedFound
Unused exported constant (LEGACY_TAX_RATE)MissedFound
Unused exported class (OldCart)MissedFound
Unused file (legacy/report.ts)MissedFound
Function in that unused file (buildReport)MissedFound
Unused exported interface (LegacyOrder)MissedMissed
Unused public method (Cart.exportCsv)MissedMissed

Neither tool flagged formatPrice, which is correct: it's used through the barrel file.

What the compiler printed:

$ tsc -p . --noUnusedLocals --noUnusedParameters
src/cart.ts(2,1): error TS6133: 'readFileSync' is declared but its value is never read.
src/cart.ts(12,11): error TS6133: 'unusedCount' is declared but its value is never read.

What Skylos printed:

$ skylos . --format concise
src/legacy/report.ts:1  SKY-U001  unused function: buildReport
src/utils/format.ts:5  SKY-U001  unused function: formatPriceLegacy
src/cart.ts:2  SKY-U002  unused import: readFileSync
src/cart.ts:21  SKY-U004  unused class: OldCart
src/utils/format.ts:9  SKY-U003  unused variable: LEGACY_TAX_RATE
src/legacy/report.ts:1  SKY-E003  Unused TypeScript/JavaScript file (not imported by any other file)

The two tools barely overlap, so running both covers more than either one alone.

Why tsc and ESLint miss unused exports

  • TypeScript's noUnusedLocals reports unused local declarations. An exported name might be imported anywhere, so the compiler doesn't treat it as unused.
  • ESLint's no-unused-vars runs one file at a time. It can't see whether another file imports an export.

Finding an unused export means building a map of every import in the project, including barrels like export * from. That's a whole-project job.

Tools that read the whole project

ToolWhat it reports (from its own docs)Languages
knip"Unused dependencies, exports and files"JavaScript and TypeScript
ts-pruneExports with no detected use elsewhere in the projectTypeScript
SkylosUnused functions, classes, variables, imports and filesTypeScript/JavaScript, Python, Java, PHP, Rust, Dart; Go through the GitHub Action (language support)

Which one to pick:

  • TypeScript only, and you also want unused dependencies: knip is built for exactly that.
  • Several languages, or you want one scan in CI: Skylos covers TypeScript alongside your Python or Java code, and the same scan checks for security issues, secrets and vulnerable dependencies.
  • Either way, keep noUnusedLocals on. It catches unused locals, which Skylos missed in this test.

Run it on your project

pip install skylos
skylos .                                             # dead code across the whole project
skylos . --diff-base origin/main --diff origin/main  # only what your branch changed

To block new dead code in pull requests, skylos cicd init writes a GitHub Actions workflow that checks the changed lines of every PR (how it works).

Before deleting anything, check for uses the scan can't see: dynamic import() with computed paths, names used only from tests or scripts outside the project, and public APIs of a published package.