Skip to content

Fix: ESLint no-unused-vars False Positives and Configuration

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix ESLint no-unused-vars false positives — TypeScript types, destructuring ignores, React imports, function arguments, and configuring the rule to match your codebase patterns.

The Error

ESLint reports unused variable errors for variables that are actually used, or for intentionally unused ones:

error  'React' is defined but never used  no-unused-vars
error  'MyType' is defined but never used  no-unused-vars
error  '_event' is defined but never used  no-unused-vars
error  'props' is defined but never used   no-unused-vars

Or TypeScript-specific false positives:

error  'FC' is defined but never used  no-unused-vars

The variable React is required for JSX transformation in older setups but isn’t referenced explicitly. MyType is a TypeScript type used only in type annotations. _event is an intentionally ignored parameter.

Why This Happens

The base ESLint no-unused-vars rule is a pure JavaScript rule. It walks the AST, finds every declared identifier, and checks whether anything in the value position references it. That works perfectly for plain JS, but it has no visibility into TypeScript’s type-only world. Type imports, interface declarations, generic parameters, and conditional type aliases all sit in a separate “type space” that the JavaScript AST does not represent. From the base rule’s perspective, those declarations are dead code.

The second category of false positives is intentional. Callback parameters often have to exist by position — you cannot skip the first argument of an event handler — but you do not always need to use them. Destructuring patterns sometimes name a property purely so that the rest spread excludes it from the new object. In both cases the variable is doing real work, just not in the way ESLint’s static analysis can see.

The third category is configuration drift. ESLint composes rules from multiple sources: shared configs, plugin recommendations, your own overrides, and parser-specific extensions. When two layers disagree about whether the rule should run, or about which rule (base vs @typescript-eslint/no-unused-vars) to run, you get phantom errors that only appear in some files or only after a dependency upgrade. The base ESLint rule doesn’t understand:

  • TypeScript types and interfacestype MyType and interface MyInterface are only used in type annotations, which no-unused-vars ignores (it’s a JavaScript rule, not TypeScript-aware).
  • React import for JSX — before React 17’s new JSX transform, import React from 'react' was required for JSX but not explicitly referenced in code.
  • Intentionally unused parameters — callback parameters you must declare by position but don’t use ((_event, value) => value).
  • Destructuring for exclusionconst { secret, ...rest } = objsecret is used to exclude it from rest, but ESLint sees it as unused.
  • Globals from other files — variables declared in one file but used via global scope in another.

Version History That Changes the Failure Mode

The unused-vars story has shifted across major ESLint releases, and the fix you reach for depends on which version you are on. ESLint 7 (mid-2020) introduced the standard .eslintrc.* config you probably remember, with extends, rules, and overrides. ESLint 8 (October 2021) kept that format and added support for eslint.config.js as an opt-in flat config. ESLint 9, released in April 2024, made flat config the default and removed the legacy config loader unless you set ESLINT_USE_FLAT_CONFIG=false. If you upgraded a project to ESLint 9 and suddenly every rule appears off or all your overrides stopped applying, you most likely still have an .eslintrc.js that the new loader ignores. See Fix: ESLint Config Not Working for the broader config-loading checklist.

The @typescript-eslint plugin has its own timeline. It superseded the old eslint-plugin-typescript and typescript-eslint-parser packages in 2019, and the v6 release in 2023 dropped support for ESLint 6 and Node 14, tightened the recommended preset, and split several rules. The v7 release (2024) aligned with TypeScript 5.x and added type-checked variants of more rules. Critically, @typescript-eslint/no-unused-vars gained destructuredArrayIgnorePattern in v5 and tracked the upstream ESLint rule’s options thereafter, so older configs that copy options from out-of-date examples may silently no-op.

TypeScript itself introduced inferred type predicates in 5.5 (mid-2024), which changes which identifiers count as “used” inside narrowing functions. The flow-typed predicate may make a variable look unused at the syntactic level while the type system still depends on it. Make sure @typescript-eslint/no-unused-vars runs against the same parser version that matches your TypeScript compiler. Finally, the community eslint-plugin-unused-imports splits the rule into two: one for unused imports (auto-fixable) and one for unused variables. If you want CI to remove dead imports on save, install that plugin and set unused-imports/no-unused-imports: error alongside the existing rule.

Fix 1: Use @typescript-eslint/no-unused-vars for TypeScript Projects

The base no-unused-vars rule doesn’t understand TypeScript. Replace it with the TypeScript-aware version:

// .eslintrc.js
module.exports = {
  extends: [
    'plugin:@typescript-eslint/recommended',
  ],
  rules: {
    // Disable the base rule — it reports false positives for TypeScript types
    'no-unused-vars': 'off',
    // Enable the TypeScript-aware version
    '@typescript-eslint/no-unused-vars': ['error', {
      argsIgnorePattern: '^_',       // Ignore args starting with _
      varsIgnorePattern: '^_',       // Ignore vars starting with _
      caughtErrorsIgnorePattern: '^_',  // Ignore caught error vars starting with _
    }],
  },
};
// .eslintrc.json equivalent
{
  "rules": {
    "no-unused-vars": "off",
    "@typescript-eslint/no-unused-vars": ["error", {
      "argsIgnorePattern": "^_",
      "varsIgnorePattern": "^_",
      "caughtErrorsIgnorePattern": "^_"
    }]
  }
}

@typescript-eslint/no-unused-vars correctly handles:

  • Type imports (import type { MyType })
  • Interfaces and type aliases used only in annotations
  • Enums used as types

Fix 2: Fix React Import False Positive

In React 17+ with the new JSX transform, you don’t need to import React for JSX. Configure your bundler/transpiler and ESLint to match:

If using React 17+ new JSX transform — remove the import and update tsconfig/babel:

// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx"  // New transform — no React import needed
  }
}
// .eslintrc.js — tell ESLint you're using the new transform
module.exports = {
  extends: [
    'plugin:react/recommended',
    'plugin:react/jsx-runtime',  // ← Disables the react/react-in-jsx-scope rule
  ],
};

If using React 16 or can’t migrate — tell ESLint that React is a global:

// .eslintrc.js
module.exports = {
  globals: {
    React: 'readonly',
  },
};

Or add React to your eslint config’s environment:

// .eslintrc.js
module.exports = {
  rules: {
    'no-unused-vars': ['error', {
      varsIgnorePattern: '^React$',
    }],
  },
};

Fix 3: Use Underscore Prefix for Intentionally Unused Variables

The convention for intentionally unused variables is to prefix them with _. Configure the rule to ignore this pattern:

// .eslintrc.js
module.exports = {
  rules: {
    '@typescript-eslint/no-unused-vars': ['error', {
      argsIgnorePattern: '^_',
      varsIgnorePattern: '^_',
      caughtErrorsIgnorePattern: '^_',
      destructuredArrayIgnorePattern: '^_',  // For array destructuring
    }],
  },
};
// Now these don't trigger the rule:

// Callback where you need the second arg but not the first
array.forEach((_item, index) => console.log(index));

// Required parameter position but unused
button.addEventListener('click', (_event) => handleClick());

// Caught error when you only want to swallow it
try {
  riskyOp();
} catch (_err) {
  // Intentionally ignored
}

// Destructuring to exclude a field from the rest
const { password: _password, ...safeUser } = user;
sendToClient(safeUser);

Pro Tip: Using _ as a prefix is more communicative than just ignoring the lint rule — it signals to readers that the variable is intentionally not used, rather than accidentally forgotten.

Fix 4: Fix Type Import False Positives

When TypeScript types are imported and only used in type positions, ESLint’s base rule flags them as unused:

// ESLint flags 'User' and 'Config' as unused
import { User, Config } from './types';

function setup(config: Config): User {  // Used in type positions only
  return { id: 1, name: 'Alice' };
}

Fix with import type syntax:

// Use 'import type' for type-only imports
import type { User, Config } from './types';
// ESLint (with @typescript-eslint rules) correctly recognizes these as type-only

Or configure the rule to handle type imports:

// .eslintrc.js
module.exports = {
  rules: {
    '@typescript-eslint/no-unused-vars': ['error', {
      // 'all' checks all variables including type parameters
      vars: 'all',
      // 'after-used' ignores unused args before the last used one
      args: 'after-used',
    }],
    // Also enable this to enforce 'import type' for type-only imports
    '@typescript-eslint/consistent-type-imports': 'error',
  },
};

Fix 5: Fix Destructuring Unused Variables

When destructuring to extract some properties and pass the rest:

// ESLint flags 'id' as unused
const { id, ...rest } = user;
return rest;  // id is used to EXCLUDE it, but ESLint doesn't know

// Fix option 1: underscore prefix
const { id: _id, ...rest } = user;

// Fix option 2: disable for that line
const { id, ...rest } = user; // eslint-disable-line @typescript-eslint/no-unused-vars

// Fix option 3: use Object.fromEntries
const { id: _, ...rest } = user;  // '_' is commonly used as throwaway name

For array destructuring:

// Skipping elements with empty slots
const [, second, , fourth] = array;

// Or with underscore convention
const [_first, second, _third, fourth] = array;

Fix 6: Disable the Rule for Specific Lines or Blocks

When the rule gives a false positive that you can’t fix with configuration, disable it inline:

// Disable for one line
const unusedButRequired = setup(); // eslint-disable-line @typescript-eslint/no-unused-vars

// Disable for the next line
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const result = initializeLibrary();

// Disable for a whole block (use sparingly)
/* eslint-disable @typescript-eslint/no-unused-vars */
const a = 1;
const b = 2;
/* eslint-enable @typescript-eslint/no-unused-vars */

// Disable for the whole file (avoid this)
/* eslint-disable no-unused-vars */

Inline disables should be used as a last resort with a comment explaining why:

// eslint-disable-next-line @typescript-eslint/no-unused-vars -- required by EventEmitter interface
const error = emitter.on('error', () => {});

Fix 7: Configure Global Variables

If variables are defined globally (via a script tag, browser globals, or a setup file) and used across files, declare them as globals to prevent false positives:

// .eslintrc.js
module.exports = {
  globals: {
    // Browser globals not in the 'browser' environment
    gtag: 'readonly',
    Stripe: 'readonly',
    __DEV__: 'readonly',
    __VERSION__: 'readonly',
  },
  env: {
    browser: true,    // window, document, fetch, etc.
    node: true,       // process, require, __dirname, etc.
    es2022: true,     // Modern JS globals
  },
};

For Jest globals:

module.exports = {
  env: {
    'jest/globals': true,  // describe, it, expect, beforeEach, etc.
  },
  plugins: ['jest'],
};

Still Not Working?

Check which rule is actually firing. The error message shows the rule name in brackets:

error  'foo' is defined but never used  no-unused-vars
                                        ^^^^^^^^^^^^^^^

If it’s no-unused-vars (base rule), switch to @typescript-eslint/no-unused-vars. If it’s already the TypeScript version but still firing, check the rule configuration.

Check ESLint config file precedence. ESLint merges configs from parent directories. A no-unused-vars: error in a root .eslintrc might override your project-level off setting:

# See what config ESLint is using for a specific file
npx eslint --print-config src/index.ts

Verify the plugin is installed:

npm list @typescript-eslint/eslint-plugin
# If missing:
npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser

Check for extends ordering. Later entries in extends override earlier ones:

// @typescript-eslint/recommended enables no-unused-vars
// If 'recommended' comes AFTER your custom rules, it overrides them
module.exports = {
  extends: [
    'plugin:@typescript-eslint/recommended',  // Enables the rule
  ],
  rules: {
    // This correctly overrides the extended rule
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
  },
};

Add eslint-plugin-unused-imports for auto-fixable cleanup. The standard rule cannot auto-fix unused imports because removing the import statement may have side effects. The community plugin separates “no unused imports” (safe to auto-fix) from “no unused vars” (warning only). With both enabled, eslint --fix strips dead imports while leaving variable warnings for you to review:

module.exports = {
  plugins: ['unused-imports'],
  rules: {
    '@typescript-eslint/no-unused-vars': 'off',
    'unused-imports/no-unused-imports': 'error',
    'unused-imports/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
  },
};

Mismatch between editor and CLI. If your editor (VS Code, JetBrains) reports different errors than npm run lint, the editor is most likely using a different ESLint version, a different working directory, or a different config file. Restart the ESLint server inside the editor and confirm both paths report the same rule set with eslint --print-config.

For related ESLint issues, see Fix: ESLint Parsing Error Unexpected Token, Fix: ESLint import/no-unresolved, and Fix: ESLint Flat Config Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles