Software engineering is a battle against complexity. Without any planning or care, it's easy to build programs where everything interacts with everything else (the "big ball of yarn" model). With a ball of yarn, if you double the number of components, you quadruple the number of interactions:
One of the best ways to fight against this ramp-up of complexity is to simply reduce N, i.e. to write fewer lines of code. Using a higher level programming language or depending on well-tested third-party libraries are common ways to do this. But one of the easiest ways is to find code you don't need any more and delete it.
Or, to quote Ken Thompson:
One of my most productive days was throwing away 1000 lines of code.
So how do you find dead code (and dead types) in a TypeScript project? There are a few ways to do it, but my current solution involves the
--noUnusedLocals compiler option and the ts-prune tool from Nadeesha Cabral.
--noUnusedLocals. This is typically set this in your
tsconfig.json file. It's on by default so its behavior should be familiar. In addition to detecting unused local variables in function bodies, it also detects unused symbols at the module level:
(In VS Code, unused symbols typically appear in a lighter color instead of getting the red squiggly underline error treatment. This is because it's often only a temporary state until you write the code that uses the symbol.)
In this example nothing calls
foo or otherwise references it, so it's dead code. And while
bar is unused in this module, it's exported, so there's at least the possibility that another module could import it and call it.
Since imported symbols are local variables inside their module,
noUnusedLocals will also catch unused imports for you.
But what about exported symbols? They're still dead code if they're unused, but determining whether that's the case is harder. It requires analysis of your whole program and it's a little trickier than you might expect at first blush. To see why, let's first look at an incorrect way to do it.
What if we just look at all the
imports in a program and pair them up with the
exports? With this approach, an
exported symbol is unused if it's never
There are several problems with this approach. Here are a few:
- Code can be
imported but still dead. Here are three ways this can happen:
- A function is only imported by its test.
- Two functions call each other (mutual recursion), but are otherwise unused.
- A function is only used by other dead code.
exported symbol can be alive even if it's never
imported. This happens if it's used in its own module.
This is all a little easier to understand in picture form:
Dead code is in gray, live code is bold. Code can be dead despite being referenced. Examples are if it's referenced from other dead code (
recB), or code that is not relevant for "liveness" like a test (
I'm saying "functions" here, but the same considerations apply to types and interfaces, too.
The first category of problems (imported but still dead) is reminiscent of a problem in garbage collection: if you do pure reference counting, then you have a problem with cycles, where two objects reference each other but are otherwise dead. The solution there is to start with some known "live" objects and follow all the references from them. This is known as mark and sweep.
We can do something similar with ts-prune. To get the most value out of it, you create a special
tsconfig.json file with a list of entrypoints to your program:
I usually call this
tsconfig.ts-prune.json. What constitutes an entrypoint depends on your program. For a Node program like a server, it's the file you run (perhaps
app.ts). For a web application, it's the entrypoint you list in your webpack or equivalent config. For a library, it's any file you want your users to be able to import from the resulting distribution (typically
Once you've created that file, you run something like this:
ts-prune at your special
tsconfig.json. Since you never
import tests, they wind up being ignored for purposes of detecting alive vs. dead code. The same goes for un-
grep filters out the second category of problems from the above list. I have no problem exporting symbols that are never imported (in fact, I recommend it in Item 47 Effective TypeScript: Export All Types That Appear in Public APIs).
(Sidebar: why? Two main reasons. First, you often wind up needing to import them later, and exporting them makes it possible for tsserver to offer auto-import. Second, you're effectively exporting them already if they're part of a public API. You may as well make it easy.)
The output of ts-prune tells us that the
bar symbol is unused despite being exported. This is exactly what we wanted to know!
I highly recommend setting up ts-prune for your project. You might find some dead code that you'd forgotten about! It also works great with more exotic setups like media imports and generated code. Two examples of this from my own projects:
- My team has a file,
icons.ts, that imports all the images in my web app, either from PNGs in my project or from Material-UI, and re-exports them. In this case running ts-prune finds the unused images, which shrinks your bundle! (Tree shaking would help here, too.)
- We use codegen to produce TypeScript types from our Postgres schema. In this case an unused export may correspond to a dead database table, which should definitely be dropped!
As with most dead code elimination tool, when you delete some dead code that it's surfaced, you should immediately run
ts-prune again. Running
tsc will surface dead references to the dead code (e.g. its tests), which you should delete. In the process, you might have deleted the last reference to something, which will surface even more dead code. Repeat until convergence (or until you have no code left!).
- ts-prune is a zero config CLI tool by Nadeehsa Cabral. I added the "used in module" output to make it more useful for finding dead code in addition to unused imports.
- ts-unused-exports is a related tool by Patricio Zavolinsky. I ran into issues with this tool that eventually led me to ts-prune, but you may prefer it if you don't like ts-prune. It certainly has many options!