React Native

Seek and destroy dead code for good: a strategy using ts-prune


Dead code designates all code that will never be executed during the app lifecycle. It can be lines of code after a return or throw instruction, or unused exported functions and components.
The former kind of dead code can be easily spotted on your IDE, but the latter is trickier: you usually don't see when an export is not used anywhere in your code anymore, and it generates all kinds of waste:

  • Waste of time: if dead code uses other code that evolves and changes shape, you will end up maintaining it for exactly no added value;
  • Waste of physical space: when your project grows, dead code clutters up your git repository, your local machine and in worst cases, your app bundle;
  • Waste of quality: you may be tempted to say some of your dead code will be of use later, but even if that is the case, by then there is a pretty good chance you quality standards will have evolved and the dead code will not be compliant to the latest standards.

As the best kind of code is the one that is not there, in this article you will find a strategy to never keep unused exports in your Typescript project.

Your best friend: ts-prune

ts-prune is a utility that finds and lists unused exports in your project, based on your tsconfig.json. It allows you to specify files to ignore, because some of them would inevitably count as dead code:

  • If you use Jest, the root-level ++code>__mocks__++/code> folder where you mock libraries;
  • Exports used by your framework but not explicitly in your code (such as App.tsx in React)
  • You can also choose to raise an error or not if dead code has been detected:

++pre> ++code> ts-prune -i App.tsx -e # will raise an error if dead code exists

ts-prune -i App.tsx # will still list dead code, but end as a success ++/pre> ++/code>

The dead code list will look like this:

++pre> ++code> src/navigation/RootNavigator.tsx:33 - RootStack (used in module)
src/navigation/RootNavigator.tsx:58 - RootNavigator
src/modules/Core/lib/theme/colors.ts:36 - default
src/modules/Core/redux/LoadingStatus/index.ts:2 - loadingStatusReducer
src/modules/Core/redux/LoadingStatus/index.ts:19 - setError ++/pre> ++/code>

(used in module) means that the code it used inside of its own file but there is no use for the export keyword.

Be careful though: as dead code designates unused exports, removing one line of dead code can reveal plenty others! I recommend you to launch ts-prune multiple times to attain your goal of cleaning your codebase.

Leverage your CI to eradicate dead code

So now we can seek and destroy dead code. But it will still reappear over time. If only we had a way toprevent my team to merge dead code...

Your favorite CI is here to save the day! Just execute ts-prune with the ++code>-e++/code> option during a the pull/merge request CI job and it will fail if the author has generated an unused export.

But sometimes, you can have too much dead code to remove in one go. In that case let's operate gradually:

1. Use a custom script to specify a maximum amount of dead code you should never reach. Like this one:

++pre> ++code> #!/bin/sh
# Let's suppose you use yarn and you have created  yarn deadcode script.
OUTPUT=`yarn --silent deadcode | wc -l`; # Count the number of unused exports
MAX_DEAD_CODE_LINES="20"; # Remember to update it regularly until it reaches 0

echo "$OUTPUT lines remaining, maximum set to $MAX_DEAD_CODE_LINES"

if [ "$OUTPUT" -gt "$MAX_DEAD_CODE_LINES" ];
echo "Error, you added dead code, please remove some."
exit 1
fi++/pre> ++/code>

2. Regularly update this maximum until it reaches zero.
3. At this point you can finally switch to the good old ++code>ts-prune -e++/code>.

This method will quickly lead to dead code eradication. You can even use the script in a pre-push Git hook to avoid using too much CI time.

Zero deadcode goal

We took our sweet time the first time I implemented it in a project but the results are here: no export was left unused.

Conclusion: mission accomplished?

1. Install ts-prune;
2. Use it in your CI to prevent anyone from generating unused exports;
3. Enjoy a codebase rid of useless functions or components.

That process works well for the most part, but it can fail to detect dead code in a particular case: when your unused function/component is tested. Indeed, the export is used in the test file - and only there, but ts-prune cannot deduce it is dead code at this point. A solution could be to entirely skip tests from time to time:

++pre> ++code> ts-prune -s '.*\.test\.tsx?'++/pre> ++/code>

Yup, you're looking at a regex.

This means ts-prune will not check imports inside test files. But that also means all your test utils (mocks, stubs, utility functions) will appear as dead code, so maybe you will have to ignore a couple more files to make the actual dead code easier to spot:

++pre> ++code> ts-prune -s '.*\.test\.tsx?' -i '.*/mocks\.tsx?' # or something like that++/pre> ++/code>

More regexes.

Also, there is a last kind of dead code that tools will have a hard time to detect. For example, I worked on an app that once had features for anonymous users, but finally decided to grant access to signed-in users only. But a lot of code pertaining to anonymous users stayed in the codebase even if was not of use anymore, because it was not easy to remove! If you work with spaghetti code, there is a pretty good chance actual working code and half-removed old features will be intertwined, leaving a lot of exports undetected by ts-prune.

I call it "zombie code", and if you stumble upon it, maybe it is time for a little hunt… or simply a rework !

Développeur mobile ?

Rejoins nos équipes