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 code | tsc --noUnusedLocals | Skylos 4.48.0 |
|---|---|---|
Unused import (readFileSync) | Found | Found |
Unused local variable (unusedCount) | Found | Missed |
Unused exported function (formatPriceLegacy) | Missed | Found |
Unused exported constant (LEGACY_TAX_RATE) | Missed | Found |
Unused exported class (OldCart) | Missed | Found |
Unused file (legacy/report.ts) | Missed | Found |
Function in that unused file (buildReport) | Missed | Found |
Unused exported interface (LegacyOrder) | Missed | Missed |
Unused public method (Cart.exportCsv) | Missed | Missed |
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
noUnusedLocalsreports unused local declarations. An exported name might be imported anywhere, so the compiler doesn't treat it as unused. - ESLint's
no-unused-varsruns 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
| Tool | What it reports (from its own docs) | Languages |
|---|---|---|
| knip | "Unused dependencies, exports and files" | JavaScript and TypeScript |
| ts-prune | Exports with no detected use elsewhere in the project | TypeScript |
| Skylos | Unused functions, classes, variables, imports and files | TypeScript/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
noUnusedLocalson. 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.