dt-clean 
Ensures the only DefinitelyTyped (@types/*) packages you have installed are the ones you need, and points out the ones you're missing.
dt-clean inspects your package.json and, for each dependency, decides what should happen to its DefinitelyTyped types package:
- add: the runtime package ships no types of its own, but a matching
@types/*package exists on the registry. - move: a
@types/*package is independencies, but belongs indevDependencies. - remove: a
@types/*package is installed but no longer needed: the runtime package now bundles its own types, or it no longer corresponds to any dependency. - keep: the
@types/*package is present and still needed.@types/nodeis always kept.
Usage
# report only (the default)
npx dt-clean [dir]
# apply the changes to package.json
npx dt-clean --update [dir]
dir is the directory containing the package.json to inspect, and defaults to the current directory.
By default, dt-clean only prints a report and changes nothing:
Package State Action Version
-------------- ------- ------ -------
@types/express present remove ^4.0.0
@types/lodash missing add ^4.17
@types/node present keep ^25.0.0
3 `@types/*` packages: 1 keep, 0 move, 1 remove, 1 add.
The State column tells you whether each DefinitelyTyped package is currently present or missing; the Action column tells you what --update would do about it.
With --update (-u), dt-clean edits package.json in place - adding, moving, and removing the relevant @types/* entries - and then reminds you to run npm install (or your package manager's equivalent) to sync node_modules.
Options
-u,--update: apply the changes topackage.json(default: report only).--setup: idempotently wire (or upgrade)dt-clean --autoin adependencieslifecycle script, without overwriting any script it did not author (see Automatic cleanup).--auto: for use in adependencieslifecycle script (run directly or vianpx) - apply the changes like--updateduringnpm install, but duringnpm cionly print what would change and exit0. Throughnpx, the script must pipe the command into stdin for thenpm cino-op to work (see Automatic cleanup).--help: show usage.
Automatic cleanup
To keep your @types/* set tidy on every install, run dt-clean --auto from a dependencies lifecycle script - the one npm's installer (arborist) runs after any operation that changes node_modules.
The one-step way to set this up, in any project, is:
npx dt-clean --setup
--setup edits package.json for you and is safe to run anywhere:
- if you have no
dependenciesscript, it adds one that runsdt-clean --autovianpx(sodt-cleanitself need not be a dependency); - if you already have one, it adds the invocation to a free
postdependencies(orpredependencies) hook instead, so your existing script is never touched - and if every hook is taken, it appends&& …to yourdependenciesscript rather than clobbering it; - if it already placed the invocation in a
post/prehook and the preferreddependenciesslot later frees up, re-running moves it back to the most-preferred available hook; - if an older form of the invocation it authored is present - a bare
dt-clean --auto, annpx-wrapped one, the POSIX-onlyDT_CLEAN_NPM_COMMANDenv prefix, or an earlier piped form with a stale version pin - re-running upgrades it in place to the current form; - if the current invocation is already in the best available hook, it does nothing; and if some other
dt-cleaninvocation (or a customized one you wrote) is already present, it leaves that alone rather than adding a duplicate.
It only ever manages this one invocation and leaves the rest of your package.json (and its formatting) alone, so it's safe to re-run - repeated runs converge on the same result. That result is the equivalent of:
{
"scripts": {
"dependencies": "node -p \"process.env.npm_command\" | npx \"dt-clean@^1.2.0\" --auto"
}
}
The node -p "process.env.npm_command" | pipe is why this looks more involved than a bare dt-clean --auto: npx (npm exec) runs dt-clean in a fresh environment where npm's own npm_command has been overwritten (to exec), so without a forward dt-clean could not tell npm install from npm ci. Node prints the real command and the pipe carries it into dt-clean's stdin, past npx. It is written this way because it has to run on every OS npm supports: no environment-variable prefix works in both POSIX shells and Windows cmd.exe, but a pipe does - and the quotes around the version keep cmd.exe from swallowing the ^. (Older setups used a DT_CLEAN_NPM_COMMAND="$npm_command" env prefix, which dt-clean still honors but which runs on POSIX shells only; --setup upgrades it to the pipe.)
Once it's wired in, dt-clean --auto decides what to do from the npm command - read straight from npm_command when invoked directly, or from the piped stdin (or the legacy DT_CLEAN_NPM_COMMAND env var) when invoked through npx:
- under
npm install, it applies the changes for you, so a fresh install keeps your@types/*set tidy automatically; - under
npm ci(typically used in CI pipelines, wherepackage.jsonmust not be mutated), it only prints what would change and exits0, so it never edits a checked-in file and never fails the install.
When invoked directly and npm_command is anything else (or absent), --auto applies the changes, exactly as under npm install. When invoked through npx without a forwarded command (for example a hand-written npx dt-clean --auto that omits the pipe), dt-clean cannot tell install from ci, so rather than risk mutating package.json during a ci it errors and exits nonzero, printing the pipe to add (or just run dt-clean --setup). It never guesses.
To avoid surprising edits, --auto runs only inside the dependencies lifecycle (or its predependencies/postdependencies hooks), or via npx: it checks npm_lifecycle_event, and if it is invoked any other way (for example directly from the shell) it refuses to do anything and exits nonzero. Use --update to apply changes manually.
On a node older than dt-clean's own engines.node, --auto skips silently and exits 0 rather than fail the install, so wiring it into a project with a wide node-version matrix is safe - the cleanup simply no-ops on the versions that can't run it. (Any other invocation on an unsupported node still prints the version it needs and exits nonzero.)
Exit codes
In the default report-only mode, the exit code is a bitmask of the kinds of pending changes, so a clean project exits 0 and you can fail CI (or a git pre-commit hook) on drift:
| Value | Meaning |
|---|---|
1 |
there are @types/* packages to remove |
2 |
there are @types/* packages to add |
4 |
there are @types/* packages to move |
The bits combine, so a project that needs both an add and a remove exits 3, and one that needs all three exits 7.
With --update, dt-clean applies the changes, leaving the project clean, so a successful run always exits 0;
a nonzero exit then means the update itself failed.
