Skip to main content
fallow dead-code performs dead code analysis on your TypeScript project. It builds a module graph from your and reports anything that isn’t reachable. Finding dead code requires building a complete module graph. Fallow does this deterministically in milliseconds, giving agents, developers, and CI pipelines the same reliable results.

Issue types

Fallow detects 17 types of dead code:
Most projects find 50+ unused exports on first run. Start with --unused-exports for the most impactful cleanup.
Here’s what typical output looks like when fallow finds multiple issue types:
$ fallow dead-code

Filtering by issue type

Report only specific issue types:

Output formats

Colored terminal output designed for readability.

Incremental analysis

Only check files changed since a git ref:
This is useful in CI to only report new issues in a pull request.
$ fallow dead-code --changed-since main

Baseline comparison

Adopt fallow incrementally by saving a baseline of existing issues:

Debugging

Trace why an export is or isn’t considered used:

How it works

Fallow uses syntactic analysis with scope-aware binding resolution via Oxc. No TypeScript compiler, no type information. That’s what makes it fast. The graph-based approach guarantees completeness regardless of project size.
Fallow works best with projects using isolatedModules: true (required for esbuild, swc, and Vite). oxc_semantic scope analysis detects unused import bindings (imports where the bound name is never read), but legacy tsc-only projects without isolatedModules may still see edge cases with type-only imports.

Script binary analysis

Fallow parses package.json scripts to detect CLI tool usage. This reduces false positives in unused dependency detection. When you have a script like "lint": "eslint src/", fallow recognizes that eslint is a binary provided by the eslint package and marks it as used. How it works:
  • Binary name to package name mapping: Script commands like tsc, vitest, or next are mapped back to their parent packages (typescript, vitest, next). These packages won’t be reported as unused even when they’re never import-ed in source code.
  • --config arguments as entry points: When a script references a config file (e.g., jest --config jest.e2e.config.ts), fallow treats that config file as an entry point. Config files won’t be flagged as unused.
  • File path arguments: Direct file references in scripts (e.g., node scripts/seed.js) are also recognized as entry points.
  • Env wrappers and package manager runners: Commands prefixed with cross-env, npx, pnpx, yarn dlx, or node -r are unwrapped to find the actual tool binary.
In this example, fallow detects typescript, vite, vitest, and eslint as used dependencies, and vitest.config.ts as an entry point.

Infrastructure entry points

Fallow scans infrastructure config files for source file references and treats them as entry points. Worker processes, migration scripts, and other infrastructure-defined files won’t be reported as unused. Supported files: Fallow searches the project root and common subdirectories (config/, docker/, deploy/) for these files.

Dynamic import resolution

Fallow resolves dynamic imports that use patterns rather than static strings. When you write import(`./locales/${lang}.json`), the import target isn’t known at analysis time. Fallow converts these patterns into glob expressions and matches them against discovered files. Supported patterns: Matched files are marked as reachable in the module graph, so they won’t be reported as unused. Useful for locale files, icon sets, route modules, and other convention-based directory structures.
Dynamic imports with fully runtime-computed paths (e.g., import(userInput)) cannot be resolved statically. Use entry in your config to mark those directories as entry points.

Re-export chain resolution

Fallow resolves export * chains through multiple levels of barrel files with cycle detection.
In this example, fallow traces the import of add in app.ts through src/index.ts and utils/index.ts back to utils/math.ts. The add export is correctly marked as used across the entire chain. Resolution handles:
  • Multi-level chains: Any depth of export * re-exports is followed until the original declaration is found.
  • Cycle detection: Circular re-export chains (e.g., a re-exports from b, b re-exports from a) are detected and handled gracefully.
  • Mixed re-exports: Named re-exports (export { foo } from './bar') and namespace re-exports (export * from './bar') are both tracked.

Namespace import narrowing

When a file uses import * as ns from './module', fallow narrows which exports are actually consumed by scanning for member accesses (ns.foo, ns.bar) and destructuring patterns (const { foo, bar } = ns) in the importing file.
Works with static imports, dynamic imports (const mod = await import('./x')), and require (const mod = require('./x')). Fallow also uses oxc_semantic scope analysis to detect imports where the binding is never read. An import { foo } from './utils' where foo is never referenced in the file does not count as a reference to the foo export. This improves unused-export detection precision.
Whole-object consumption patterns like Object.values(ns), { ...ns }, for (const k in ns), and rest destructuring (const { a, ...rest } = ns) conservatively mark all exports as used. Fallow can’t determine which specific members are accessed in these cases.

Class member detection

Fallow detects unused public class members (methods and properties) that are never referenced outside their defining class. Unlike simple text matching, fallow understands class inheritance, decorators, and framework conventions.
What fallow handles automatically:
  • Inheritance: A method defined on a parent class and called via the parent type credits the child’s override as used
  • Decorators: Decorated members are excluded from detection by default. Decorators like @Get(), @Column(), @Injectable() indicate runtime wiring
  • Framework lifecycle methods: componentDidMount, ngOnInit, connectedCallback, and other framework lifecycle methods are never flagged
  • Whole-object patterns: Object.values(instance), Object.keys(), spread operators, and for..in loops conservatively mark all members as used

Opting decorators out via ignoreDecorators

If you use utility decorators that do NOT imply reflective consumption (Playwright’s @step("label"), internal labeling decorators like @measure, @log, @retry), list their names in the ignoreDecorators config option so methods decorated with ONLY those names are checked for usage like undecorated methods.
Conservative semantics: a method carrying any decorator NOT in the list still gets skipped, so @step combined with @Inject on the same method stays treated as framework-managed. Matching rule:
  • Entries containing . ("decorators.log") match the full dotted decorator path.
  • Bare entries ("step" or "decorators") match the leftmost segment, so a single bare "decorators" entry collapses an entire @decorators.* namespace.
  • Both "@step" and "step" round-trip equivalently (a leading @ is stripped before matching).
Unmatched entries (a decorator name in the config that never appears in your codebase) surface as a one-time warning at end of run, mirroring the existing usedClassMembers behavior. The default empty list preserves today’s skip-all behavior, so existing NestJS / Angular / TypeORM projects see no change.
Class member detection works via syntactic analysis, without invoking the TypeScript compiler. This means fallow tracks member access through the import graph, not through type resolution.

CSS and SCSS tracking

Fallow tracks CSS and SCSS imports using dedicated AST-based extraction. SCSS @use, @forward, and partial imports (_prefix files) are resolved accurately. CSS Module class names are extracted as named exports and tracked through styles.className member accesses. See CSS, SCSS, and Tailwind analysis for full details.

Entry-point partial unused exports

When a file is an entry point (matched by a plugin or the entry config), fallow traditionally marks all its exports as used. Starting in v2.15.0, fallow can detect partially unused exports in entry-point files: exports that exist in an entry file but are never imported by any other module in the project. This is especially useful for framework convention files (Next.js pages, SvelteKit routes) where the framework consumes specific named exports (like default, loader, or getStaticProps) but the file may also export helper functions that nothing uses.
Entry-point files are still considered used (never reported as unused files), but individual exports within them that have zero references are now reported as unused exports.

Cross-reference with duplication

Running fallow dead-code --include-dupes cross-references dead code findings with code duplication analysis. Clone instances in unused files, or overlapping with unused exports, are flagged as combined high-priority findings.
Use --include-dupes to prioritize cleanup: if a block of code is both duplicated and unused, removing it eliminates dead code and reduces duplication at the same time. What the cross-reference finds:
  • Clone instances in unused files: If a file is unreachable from entry points and contains duplicated code, the duplication finding is elevated.
  • Clone instances overlapping unused exports: If an unused export contains code that is duplicated elsewhere, both findings are reported together.
Use --include-dupes in CI to surface code that is both unused and duplicated.

Circular dependency benchmarks

This is not a like-for-like comparison. fallow dead-code --circular-deps runs the full analysis pipeline (dead code, dependencies, boundaries, cycles) while madge and dpdm only build an import graph. dpdm reports incomplete cycle counts on these fixtures, so its timings are not directly comparable. Cold runs, fastest tool per row in bold.
Fallow is faster on small and mid-size projects and on the large TypeScript repo versus madge, while madge wins on large monorepos like next.js and astro. dpdm is fast but reports incomplete cycle counts on these fixtures. Fallow reuses the module graph already built for dead code analysis, so cycle detection adds no extra graph build.

See also

CLI: dead-code

Full reference for the fallow dead-code command and its flags.

Rules & Severity

Control which issue types are errors, warnings, or disabled.

Auto-fix

Automatically remove the dead code fallow finds.