Maintain Large TypeScript Codebases
Learn practical strategies for keeping large TypeScript codebases healthy, from dependency management and dead code removal to compiler performance tuning.
TypeScript codebase maintenance is the ongoing work of stopping a project that compiles in three seconds today from taking thirty seconds in six months. Not because anyone made a mistake, but because incremental decay is the default.
A file added here, a new dependency there, a barrel file that grew from three exports to forty. None of it breaks the build, so none of it gets attention.
Maintaining a large codebase means actively fighting this decay. The good news is that TypeScript gives you the tools to measure, detect, and fix every form of rot before it becomes a crisis.
Keep Dependencies Lean
The single biggest factor in compilation speed and project complexity is how many files TypeScript has to process. Every import adds a file to the compilation, and every barrel file multiplies that cost.
Every unnecessary dependency is a tax paid on every build.
Audit your dependencies regularly. Look for packages in package.json that are no longer imported anywhere.
Look for imports in your codebase that pull in large modules when only a small part is needed, and consider replacing heavy dependencies with lighter alternatives that do one thing well.
Inside your own codebase, limit what each module imports. A file that imports from fifteen other modules is harder to understand, harder to test, and slower to compile than a file that imports from three. For more on controlling imports, see set package boundaries in TypeScript.
Remove Dead Code Automatically
Dead code is code that nothing imports. It compiles, it gets type-checked, it takes up space in your editor's autocomplete results, and it serves no purpose. Over time, dead code accumulates because removing it feels risky and nobody is sure whether something still uses it.
Knip analyzes your project's import graph and identifies exports that nothing imports. The older ts-prune tool did the same job but was archived in 2025, so Knip is now the maintained choice.
Run it in CI and treat every flagged export as a bug to fix. The rule is simple: if nothing imports it, delete it.
TypeScript itself helps with local dead code. Enable noUnusedLocals and noUnusedParameters in your tsconfig.
These flags catch unused variables and function parameters inside each file. They cost nothing and prevent dead code at the smallest level:
{
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true
}
}A variable declared but never read is clutter. A parameter passed but never used is a lie about what the function needs. Both should be caught by the compiler, not by code review.
Split Large Projects with Project References
As a codebase grows, compiling everything as a single project becomes the bottleneck. TypeScript processes every file on every change, even if the change only affects one corner of the codebase. Project references solve this by splitting the codebase into independently compiled projects.
Each package or domain area becomes its own TypeScript project with composite mode enabled. The root tsconfig references each project. When you change a file, only the project containing that file and the projects that directly depend on it rebuild.
A change in the shared package triggers rebuilds of both apps because both depend on shared. A change in app-web triggers only its own rebuild because nothing else depends on it. The build graph ensures that only affected projects recompile, saving minutes on every incremental build.
For the full setup, see use monorepos with TypeScript.
Tighten Compiler Options Incrementally
Every strict compiler flag you enable catches a category of bugs before they happen. But enabling all of them at once on an existing codebase produces hundreds of errors and demoralizes the team. The better approach is to enable strict flags one at a time.
Start with strictNullChecks. It forces every function to handle the possibility of null and undefined, which eliminates the most common runtime error in JavaScript. Commit the changes, let the team adjust, then move to the next flag.
The progression that most teams follow is: strictNullChecks first, then noImplicitAny, then strictFunctionTypes, then noUncheckedIndexedAccess. Each flag reveals real bugs, and fixing them improves the codebase incrementally rather than in one overwhelming batch.
Profile Compilation Performance
When compilation is slow, guessing at the cause wastes time. TypeScript can generate a detailed performance trace that shows exactly where time is being spent. Run tsc with the generateTrace flag to produce a trace file that you can analyze in Chrome's tracing tools:
tsc --generateTrace trace-dirThe trace shows which files take the longest to type-check, which types are the most expensive to resolve, and where the compiler spends its time overall. Armed with this data, you can target the slowest files or types for optimization instead of guessing.
Common findings from traces include: a single file with an excessively complex generic type that takes seconds to resolve, a barrel file that causes the compiler to load and check fifty files for every import, or a large third-party type definition that is checked on every build despite never changing.
Automate Hygiene Checks
The best way to maintain a codebase is to prevent problems from being introduced in the first place. Automate every check that matters and run it in CI.
A human reviewer might miss a new dependency on a deprecated package or an import that crosses a forbidden boundary. A script never misses.
Essential automated checks for a TypeScript project include enforcing dependency direction with ESLint, checking for dead exports with Knip, running the full type check with strict mode enabled, and measuring bundle size impact for frontend changes.
These checks should be fast enough to run on every pull request. If a check takes more than a minute, it is too slow and will be ignored. Cache results where possible and only run expensive checks on changed files.
Common Mistakes
Ignoring compilation warnings. TypeScript warnings are compiler errors waiting to happen. The noImplicitAny flag starts as a warning and becomes an error when the flag is enabled. Fix warnings as they appear, not when they become blockers.
Letting barrel files grow without limit. A barrel file that re-exports from 50 modules forces every consumer to resolve all 50. Split large barrels into focused, domain-specific entry points.
Upgrading TypeScript without reading the release notes. Each major version adds new strictness checks under the existing strict flag. A project that compiles on TypeScript 5.5 might fail on 5.6 because strict mode got stricter. Read the release notes and plan time for fixes.
Running full builds instead of incremental. tsc --build caches compilation results per project. Running plain tsc on a monorepo root recompiles everything from scratch. Always use build mode for large projects.
For more on keeping refactors safe as the codebase evolves, see refactor a TypeScript codebase safely.
Rune AI
Key Insights
- Run dead export detection in CI to prevent unused code from accumulating over time.
- Use project references to split large codebases into independently compiled packages.
- Audit dependencies periodically: remove unused packages and keep shared types minimal.
- Enable strict compiler options incrementally; each new flag catches a category of bugs.
- Measure compilation time with --generateTrace and optimize the slowest parts first.
Frequently Asked Questions
How do I find unused code in a large TypeScript project?
Why is my TypeScript compilation getting slower over time?
Conclusion
Large TypeScript codebases do not decay by accident. They decay when dead code accumulates, dependencies grow unchecked, and compilation slows to a crawl. The fixes are mechanical and repeatable: measure what is there, remove what is unused, split what is too large, and automate the checks so the rot never starts.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.