noUnusedLocals
Reports an error on declared local variables that are never read.
"noUnusedLocals": boolean // default: false- Added in TypeScript 2.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$noUnusedLocalsoptional | boolean= false | Report errors on unused local variables. |
$noUnusedParametersoptional | boolean= false | Report errors on unused function parameters. Parameters whose names begin with an underscore are exempt. |
Return value
booleanAn error on each local declaration whose value is never read.
Errors
Error TS6133, "'x' is declared but its value is never read", for each offending declaration.
Changelog
- PHP 2.0Introduced together with noUnusedParameters and the leading-underscore exemption for parameters.
- PHP 2.9Diagnostics are reported as suggestions rather than hard errors in editors for some cases, so a file can look clean in the IDE and still fail tsc.
noUnusedLocals turns an unread local declaration into a compile error, TS6133. Its partner noUnusedParameters does the same for function parameters. Both catch the residue of refactoring: the import you stopped using, the variable left over after a branch was deleted, the destructured field nothing reads.
The underscore convention
Some parameters exist because a signature demands them — a callback that only wants the third argument, an interface method you must match. Prefixing the name with _ exempts it: (_req, res) => ... compiles clean under noUnusedParameters. This is a documented compiler rule, not a lint plugin.
What trips people up
The exemption is for parameters. A local variable named _temp still errors under noUnusedLocals. And these are errors, not warnings — there is no severity dial, so a half-finished function with a variable you are about to use fails the build. Many teams therefore leave the compiler flags off and use ESLint's @typescript-eslint/no-unused-vars, which is configurable as a warning, auto-fixable, and understands the same underscore pattern. Pick one; running both just doubles the noise.
noUnusedParameters also ignores parameters that precede a used one only when they are underscore-prefixed, so name them deliberately.
Example
type Row = { id: number; label: string };
// _pageIndex is required by the caller's signature but unused here.
// The leading underscore exempts it from noUnusedParameters.
function summarize(rows: Row[], _pageIndex: number): string[] {
const prefix = "row"; // read below -> fine
const unusedTotal = rows.length; // TS6133: declared but never read
return rows.map((r) => `${prefix}:${r.id}`);
}
console.log(summarize([{ id: 1, label: "a" }, { id: 2, label: "b" }], 0));
// Renaming unusedTotal to _unusedTotal does NOT help:
// the underscore rule covers parameters, not locals.When to use it
- Failing CI on imports and variables left behind by a refactor.
- Keeping a required-but-unused callback parameter with an underscore prefix, as in (_req, res).
- Catching a destructured property nobody reads after a schema change.
- Removing dead code before publishing a library, so no unused symbol reaches the bundle.
Frequently asked questions
How do I silence "declared but its value is never read" for a parameter I must keep?
Prefix its name with an underscore, for example (_event, context) => .... TypeScript exempts underscore-prefixed parameters from noUnusedParameters by design. The same trick does not work for local variables under noUnusedLocals — there you have to delete the declaration or actually use it.
Should I use noUnusedLocals or the ESLint rule?
Use the ESLint rule if you want warnings rather than build failures, automatic fixing, and the underscore convention extended to variables and caught errors. Use the compiler flag if you want it strictly enforced with no configuration and no extra tooling. Running both produces duplicate diagnostics for the same code.
Why does my editor show no error but tsc fails with TS6133?
Editors often surface unused-symbol diagnostics as greyed-out suggestions rather than red errors, so the file looks clean while the command-line build fails. Run tsc --noEmit locally before pushing, or configure your editor to display these as errors, so the two agree.
More examples
Underscores where they do and do not work
The handler compiles because _req is a parameter. The rest-destructuring idiom is the standard way to strip a field while satisfying the checker. _scratch still errors, which is the distinction people miss. Output shows "ok" and the user object without the password field.
// OK under noUnusedParameters: the parameter is underscore-prefixed.
const handler = (_req: { url: string }, res: { send: (s: string) => void }) => {
res.send("ok");
};
// OK: unused destructured siblings are exempt when you name the rest.
const { password: _password, ...safeUser } = {
id: 7, name: "ada", password: "secret",
};
// NOT ok under noUnusedLocals: a local is a local, underscore or not.
const _scratch = safeUser.id * 2; // TS6133
let sent = "";
handler({ url: "/" }, { send: (s) => { sent = s; } });
console.log(sent, safeUser);Compiler flags versus the lint rule
Option A gives a hard error with no severity control, which interrupts work-in-progress code. Option B can warn instead of erroring, applies the underscore convention to variables and caught errors as well as arguments, and removes the offenders with --fix. Most teams pick one; enabling both reports every unused symbol twice.
// Option A — the compiler enforces it, no exceptions, build fails.
{
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
// Option B — ESLint enforces it, configurable and auto-fixable.
{
"rules": {
"@typescript-eslint/no-unused-vars": ["warn", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}]
}
}
Discussion