Learn why TypeScript throws TS6133 when noUnusedLocals or noUnusedParameters finds a declaration nothing reads, and how to clean it up safely.
TS6133 means you declared something and then never read it. The import at the top of the file, the const you added while debugging, the callback parameter you kept only to reach the second one — TypeScript found the declaration, walked the rest of the file, and never saw anyone use the value.
The check is driven by two compiler options that are not part of strict: noUnusedLocals covers imports, local variables, destructured bindings and type parameters, while noUnusedParameters covers function parameters. Turn either on and a codebase that compiled fine yesterday can light up with dozens of these. This is a dead-code hygiene rule, not a type-safety rule — nothing about the program is unsound, it is just carrying weight it does not need.
The wording matters. Since TypeScript 2.6 the message says its value is never read rather than the older is declared but never used, because writing to a variable does not count as using it:
export function saveDraft(title: string) {
let lastSavedTitle = ""
// ~~~~~~~~~~~~~~ Error: 'lastSavedTitle' is declared but its value is never read. (TS6133)
lastSavedTitle = title // a write, not a read — still unused
return title.trim()
}By far the most common trigger. You removed the last call to a helper or a hook but left it in the import list. Every named import counts as a local, so noUnusedLocals flags it.
// ❌ Broken — with "noUnusedLocals": true
import { useState, useEffect } from "react"
// ~~~~~~~~~ Error: 'useEffect' is declared but its value is never read. (TS6133)
export function CartBadge() {
const [itemCount] = useState(0)
return <span>{itemCount}</span>
}// ✅ Fixed — drop the specifier you no longer use
import { useState } from "react"
export function CartBadge() {
const [itemCount] = useState(0)
return <span>{itemCount}</span>
}If every specifier in one import statement is unused, the compiler reports TS6192 for the whole declaration instead of naming each one.
A value computed during a refactor and then orphaned. The code still runs — it just does arithmetic nobody looks at.
// ❌ Broken — with "noUnusedLocals": true
export function orderTotal(prices: number[]) {
const taxRate = 0.2
// ~~~~~~~ Error: 'taxRate' is declared but its value is never read. (TS6133)
return prices.reduce((sum, price) => sum + price, 0)
}// ✅ Fixed — use the value (or delete the line if the tax really does not belong here)
export function orderTotal(prices: number[]) {
const taxRate = 0.2
const subtotal = prices.reduce((sum, price) => sum + price, 0)
return subtotal * (1 + taxRate)
}Positional parameters cannot be skipped. To read rowId you must declare whatever comes before it, even when the handler ignores it — and noUnusedParameters complains.
// ❌ Broken — with "noUnusedParameters": true
export const onRowClick = (event: MouseEvent, rowId: string) => {
// ~~~~~ Error: 'event' is declared but its value is never read. (TS6133)
selectRow(rowId)
}// ✅ Fixed — the underscore prefix marks it as deliberately unused
export const onRowClick = (_event: MouseEvent, rowId: string) => {
selectRow(rowId)
}The escape hatch is real but narrow: noUnusedParameters exempts any parameter whose name starts with _. Locals get no such exemption — a const _taxRate is still reported.
Pulling several properties out of an object and then using one of them reports each unused binding by name.
// ❌ Broken — with "noUnusedLocals": true
export function userLabel(user: { id: string; name: string }) {
const { id, name } = user
// ~~~~ Error: 'name' is declared but its value is never read. (TS6133)
return id
}// ✅ Fixed — destructure only what you read
export function userLabel(user: { id: string; name: string }) {
const { id } = user
return id
}Two neighbours worth knowing. If no binding in the pattern is read, you get TS6198 for the whole pattern. And the object-rest omit idiom is deliberately exempt — the compiler stays quiet about passwordHash here, because dropping a property is the point of the pattern:
// ✅ No error — a rest element makes the omitted binding intentional
export function publicUser(user: { id: string; passwordHash: string; email: string }) {
const { passwordHash, ...safeUser } = user
return safeUser
}Delete the declaration. The right fix nine times out of ten. An unused import, variable or binding is dead weight; removing it shrinks the bundle and the file. Most editors do it in one action — "Organize Imports" in VS Code strips unused specifiers across the whole file.
Use the value if it was supposed to be used. TS6133 is sometimes a genuine bug report: you computed taxRate and then forgot to multiply by it. Read the surrounding function before deleting — the compiler may have caught a missing line rather than a spare one.
Prefix intentionally unused parameters with _. For positional callback parameters, (_event, rowId) => ... is the idiomatic signal that the omission is on purpose:
[object Object]This works for parameters only. Renaming an unused local to _taxRate changes nothing.
Pick one owner for the rule, not two. If your project already runs @typescript-eslint/no-unused-vars, you may prefer to leave noUnusedLocals and noUnusedParameters off so the same problem is not reported twice with different escape hatches. Choose deliberately — turning the compiler flags off because an error appeared, with no linter in place, just hides the dead code again. And never reach for // @ts-ignore here: it suppresses the diagnostic without removing a single byte of the unused code.
Keep skipLibCheck: true. With library checking on, unused declarations inside a dependency's .d.ts files can fail your build for code you do not own. skipLibCheck: true is the default in modern setups for exactly this reason, and it leaves your own sources fully checked.
The compiler's unused-declaration check runs over every module and function body and reports anything nothing reads: imports, local variables, destructured bindings, type parameters, private class members and function parameters. Two options control it — noUnusedLocals for the first group and noUnusedParameters for parameters — and neither is enabled by strict, so the error usually appears the day someone adds a flag to tsconfig.json or CI runs a different config than your editor. A write with no matching read still counts as unused, which is why the message says "its value is never read".
Rename the parameter with a leading underscore. noUnusedParameters skips any parameter matching _name, so (_event: MouseEvent, rowId: string) => selectRow(rowId) compiles cleanly while documenting that ignoring the first argument was a decision. The exemption is scoped to parameters: unused locals, imports and destructured bindings are reported no matter what you call them, and the fix there is to delete them or use them. If you want the rule to apply per-file rather than per-name, move the check to ESLint's no-unused-vars with argsIgnorePattern and turn the compiler flag off.
Because TS6133 belongs to the compiler's suggestion category. The language service reports suggestions to your editor regardless of the flags in tsconfig.json, which is how the unused name gets greyed out and shows ts(6133) on hover. With the flags off, that hint has no effect on your build: tsc --noEmit exits zero and the file compiles. It only becomes an error that fails tsc once noUnusedLocals or noUnusedParameters is switched on. To stop the greying without touching your compiler config, disable the unused-code hints in your editor's TypeScript settings.
Browse all TypeScript practice challenges to keep sharpening your type-level skills.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges