DEV Community

Tarun Moorjani
Tarun Moorjani

Posted on

Setting Up TypeScript ESLint Rules Teams Actually Follow

Setting Up TypeScript ESLint Rules Teams Actually Follow

I've seen two types of ESLint configs:

Type 1: The Abandoned Config

{
  "rules": {
    "no-console": "error",
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/explicit-function-return-type": "error",
    "react/prop-types": "error"
  }
}
Enter fullscreen mode Exit fullscreen mode

Result: Developers disable ESLint entirely because it's "too annoying." The codebase has // eslint-disable comments everywhere. The config sits unused.

Type 2: The "Whatever" Config

{
  "extends": ["eslint:recommended"]
}
Enter fullscreen mode Exit fullscreen mode

Result: ESLint catches nothing useful. Bugs slip through. The team wonders why they even have ESLint.

After 5 years of tuning ESLint configs across multiple teams, I've found the sweet spot: rules that prevent real bugs without driving developers crazy.

Here's the config that actually works.

The Philosophy: Three Tiers

Tier 1: Rules That Catch Bugs (Always Enable)

These prevent production incidents. Never disable them.

Tier 2: Rules That Improve Quality (Enable, But Configurable)

These catch code smells and maintainability issues. Worth enabling, but might need team discussion.

Tier 3: Rules That Annoy Developers (Usually Disable)

These enforce style preferences that don't prevent bugs. Let Prettier handle formatting.

The Base Configuration

Start here. This works for 90% of teams.

npm install --save-dev \
  eslint \
  @typescript-eslint/eslint-plugin \
  @typescript-eslint/parser \
  eslint-plugin-react \
  eslint-plugin-react-hooks \
  eslint-plugin-jsx-a11y \
  eslint-config-prettier
Enter fullscreen mode Exit fullscreen mode
// .eslintrc.json
{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2022,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    },
    "project": "./tsconfig.json"
  },
  "plugins": [
    "@typescript-eslint",
    "react",
    "react-hooks",
    "jsx-a11y"
  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:jsx-a11y/recommended",
    "prettier" // Must be last
  ],
  "rules": {
    // Our custom rules go here
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Tier 1: Bug-Preventing Rules (Always Enable)

These rules have caught real bugs in production. Never disable them.

1. @typescript-eslint/no-floating-promises

What it catches:

// ❌ Promise rejection unhandled - app crashes
async function deleteUser(id: string) {
  await api.delete(`/users/${id}`);
}

function handleClick() {
  deleteUser(userId); // Forgot await! Error disappears silently
}

// ✅ Forces you to handle it
async function handleClick() {
  await deleteUser(userId);
  // or
  deleteUser(userId).catch(handleError);
  // or explicitly ignore
  void deleteUser(userId);
}
Enter fullscreen mode Exit fullscreen mode

Configuration:

{
  "rules": {
    "@typescript-eslint/no-floating-promises": "error"
  }
}
Enter fullscreen mode Exit fullscreen mode

Why it matters: Unhandled promise rejections crash Node.js and cause silent failures in browsers.

2. @typescript-eslint/no-misused-promises

What it catches:

// ❌ Async function in event handler - doesn't wait
<button onClick={async () => {
  await saveData();
  console.log('Saved!'); // This runs, but React doesn't wait
}}>Save</button>

// ❌ Using promise in conditional
if (fetchData()) { // Always truthy! fetchData returns Promise
  // This always runs, even if fetch fails
}

// ✅ Correct
const handleClick = async () => {
  await saveData();
  console.log('Saved!');
};

<button onClick={handleClick}>Save</button>

// ✅ Correct
const data = await fetchData();
if (data) {
  // Now checking actual data
}
Enter fullscreen mode Exit fullscreen mode

Configuration:

{
  "rules": {
    "@typescript-eslint/no-misused-promises": [
      "error",
      {
        "checksVoidReturn": true,
        "checksConditionals": true
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

3. react-hooks/exhaustive-deps

What it catches:

// ❌ Stale closure bug
function Component({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []); // Missing userId!

  // User never updates when userId changes
}

// ✅ Correct dependencies
useEffect(() => {
  fetchUser(userId).then(setUser);
}, [userId]);
Enter fullscreen mode Exit fullscreen mode

Configuration:

{
  "rules": {
    "react-hooks/exhaustive-deps": "error"
  }
}
Enter fullscreen mode Exit fullscreen mode

Why it matters: Stale closures are the #1 cause of bugs in hooks-based React apps.

4. @typescript-eslint/no-unnecessary-type-assertion

What it catches:

// ❌ Lying to TypeScript
const value = apiResponse as User; // No validation!

// If API shape changes, TypeScript can't help
value.newProperty; // Runtime error

// ✅ Use type guards instead
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'name' in value
  );
}

if (isUser(apiResponse)) {
  apiResponse.name; // Safe
}
Enter fullscreen mode Exit fullscreen mode

Configuration:

{
  "rules": {
    "@typescript-eslint/no-unnecessary-type-assertion": "error"
  }
}
Enter fullscreen mode Exit fullscreen mode

5. @typescript-eslint/await-thenable

What it catches:

// ❌ Awaiting non-promise
async function example() {
  await nonAsyncFunction(); // Does nothing, misleading
  const value = await synchronousValue; // Not a promise
}

// ✅ Only await actual promises
async function example() {
  nonAsyncFunction(); // No await needed
  const value = synchronousValue; // No await needed
  await actualAsyncFunction(); // This needs await
}
Enter fullscreen mode Exit fullscreen mode

Configuration:

{
  "rules": {
    "@typescript-eslint/await-thenable": "error"
  }
}
Enter fullscreen mode Exit fullscreen mode

6. @typescript-eslint/no-unsafe-assignment

What it catches:

// ❌ Unsafe assignment from any
function processData(data: any) {
  const user: User = data; // No validation!
  user.email.toLowerCase(); // Crashes if data.email is undefined
}

// ✅ Validate before assigning
function processData(data: unknown) {
  const validated = UserSchema.parse(data);
  validated.email.toLowerCase(); // Safe
}
Enter fullscreen mode Exit fullscreen mode

Configuration:

{
  "rules": {
    "@typescript-eslint/no-unsafe-assignment": "warn", // Warn first, error later
    "@typescript-eslint/no-unsafe-member-access": "warn",
    "@typescript-eslint/no-unsafe-call": "warn",
    "@typescript-eslint/no-unsafe-return": "warn"
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: Start with "warn" because existing code might have many violations. Upgrade to "error" over time.

7. @typescript-eslint/no-unused-vars

What it catches:

// ❌ Dead code
function calculate(a: number, b: number, c: number) {
  return a + b; // c is never used - probably a bug
}

// ❌ Unused imports slow down bundle
import { ComponentA, ComponentB, ComponentC } from './components';
// Only using ComponentA

// ✅ Clean code
function calculate(a: number, b: number) {
  return a + b;
}

import { ComponentA } from './components';
Enter fullscreen mode Exit fullscreen mode

Configuration:

{
  "rules": {
    "@typescript-eslint/no-unused-vars": [
      "error",
      {
        "argsIgnorePattern": "^_",
        "varsIgnorePattern": "^_",
        "caughtErrorsIgnorePattern": "^_"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Pro tip: Use _ prefix for intentionally unused variables:

function onClick(_event: MouseEvent) {
  // Don't need event, but required by interface
}
Enter fullscreen mode Exit fullscreen mode

8. react-hooks/rules-of-hooks

What it catches:

// ❌ Conditional hooks
function Component({ condition }) {
  if (condition) {
    const data = useFetch('/api/data'); // Breaks rules of hooks
  }
}

// ❌ Hooks in loops
items.map(item => {
  const data = useFetch(item.url); // Breaks rules of hooks
});

// ✅ Hooks at top level
function Component({ condition }) {
  const data = useFetch(condition ? '/api/data' : null);
}
Enter fullscreen mode Exit fullscreen mode

Configuration:

{
  "rules": {
    "react-hooks/rules-of-hooks": "error"
  }
}
Enter fullscreen mode Exit fullscreen mode

Tier 2: Quality-Improving Rules (Recommended)

These improve code quality but might need discussion with your team.

1. @typescript-eslint/no-explicit-any

The debate:

// Some teams: "any is never okay"
// Other teams: "any is fine for prototypes"
Enter fullscreen mode Exit fullscreen mode

My recommendation: Warn, not error. Allow any with justification.

{
  "rules": {
    "@typescript-eslint/no-explicit-any": "warn"
  }
}
Enter fullscreen mode Exit fullscreen mode

When any is acceptable:

  • Working with truly dynamic data (e.g., JSON.parse)
  • Prototyping (temporary)
  • Interfacing with untyped libraries (until you add types)

Add a comment when you use any:

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = JSON.parse(response);
// TODO: Add proper type validation with Zod
Enter fullscreen mode Exit fullscreen mode

2. @typescript-eslint/no-non-null-assertion

What it catches:

// ❌ Risky non-null assertion
const user = users.find(u => u.id === userId)!;
user.name; // Crashes if user not found

// ✅ Proper null handling
const user = users.find(u => u.id === userId);
if (!user) throw new Error('User not found');
user.name; // Safe
Enter fullscreen mode Exit fullscreen mode

Configuration:

{
  "rules": {
    "@typescript-eslint/no-non-null-assertion": "warn"
  }
}
Enter fullscreen mode Exit fullscreen mode

When ! is acceptable:

  • After explicit null checks
  • In tests where you control the data
  • When you're 100% certain (document why)
// Acceptable use
const element = document.getElementById('root');
if (!element) throw new Error('Root element not found');

// Now we're certain
ReactDOM.render(<App />, element!);
Enter fullscreen mode Exit fullscreen mode

3. @typescript-eslint/explicit-function-return-type

The debate: Should functions require explicit return types?

My take: No for private functions, yes for public APIs.

{
  "rules": {
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/explicit-module-boundary-types": "warn"
  }
}
Enter fullscreen mode Exit fullscreen mode

Why:

// ❌ Too verbose for internal code
const add = (a: number, b: number): number => a + b; // Obvious return type

// ✅ Let inference work
const add = (a: number, b: number) => a + b; // TypeScript infers number

// ✅ Explicit for public APIs
export function processUser(user: User): ProcessedUser {
  // Return type is part of the contract
  return { ...user, processed: true };
}
Enter fullscreen mode Exit fullscreen mode

4. @typescript-eslint/consistent-type-imports

What it does:

// Before
import { User } from './types';
import { api } from './api';

// After
import type { User } from './types';
import { api } from './api';
Enter fullscreen mode Exit fullscreen mode

Why it matters:

  • Smaller bundles (type imports are stripped)
  • Clearer separation of types vs values
  • Faster TypeScript compilation

Configuration:

{
  "rules": {
    "@typescript-eslint/consistent-type-imports": [
      "warn",
      {
        "prefer": "type-imports",
        "fixable": true
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

5. @typescript-eslint/prefer-nullish-coalescing

What it catches:

// ❌ Logical OR treats 0, '', false as falsy
const count = userInput || 0; // If user enters 0, returns 0 (wrong!)
const name = user.name || 'Anonymous'; // If name is '', returns 'Anonymous'

// ✅ Nullish coalescing only treats null/undefined as falsy
const count = userInput ?? 0; // If user enters 0, returns 0 (correct!)
const name = user.name ?? 'Anonymous'; // Only uses default if null/undefined
Enter fullscreen mode Exit fullscreen mode

Configuration:

{
  "rules": {
    "@typescript-eslint/prefer-nullish-coalescing": "warn"
  }
}
Enter fullscreen mode Exit fullscreen mode

6. @typescript-eslint/prefer-optional-chain

What it catches:

// ❌ Old way
const email = user && user.profile && user.profile.email;

// ✅ Optional chaining
const email = user?.profile?.email;
Enter fullscreen mode Exit fullscreen mode

Configuration:

{
  "rules": {
    "@typescript-eslint/prefer-optional-chain": "warn"
  }
}
Enter fullscreen mode Exit fullscreen mode

7. jsx-a11y/* (Accessibility Rules)

Start with recommended, customize as needed:

{
  "extends": [
    "plugin:jsx-a11y/recommended"
  ],
  "rules": {
    // Enforce label association
    "jsx-a11y/label-has-associated-control": "error",

    // Require alt text
    "jsx-a11y/alt-text": "error",

    // Warn on missing ARIA labels (not always needed)
    "jsx-a11y/aria-label": "warn",

    // Some rules are too strict for real apps
    "jsx-a11y/no-autofocus": "off", // Sometimes you DO want autofocus
    "jsx-a11y/click-events-have-key-events": "warn" // Warn, don't block
  }
}
Enter fullscreen mode Exit fullscreen mode

Tier 3: Rules to Disable (Annoying Without Value)

These rules sound good in theory but cause frustration in practice.

1. @typescript-eslint/explicit-function-return-type (Too Verbose)

// ❌ This rule forces:
const add = (a: number, b: number): number => a + b;

// ✅ TypeScript already knows the return type
const add = (a: number, b: number) => a + b;
Enter fullscreen mode Exit fullscreen mode

Disable it:

{
  "rules": {
    "@typescript-eslint/explicit-function-return-type": "off"
  }
}
Enter fullscreen mode Exit fullscreen mode

2. react/prop-types (Redundant with TypeScript)

// ❌ This rule forces PropTypes when you have TypeScript
interface ButtonProps {
  label: string;
  onClick: () => void;
}

function Button({ label, onClick }: ButtonProps) {
  return <button onClick={onClick}>{label}</button>;
}

Button.propTypes = { // Redundant!
  label: PropTypes.string.isRequired,
  onClick: PropTypes.func.isRequired,
};
Enter fullscreen mode Exit fullscreen mode

Disable it:

{
  "rules": {
    "react/prop-types": "off"
  }
}
Enter fullscreen mode Exit fullscreen mode

3. @typescript-eslint/naming-convention (Too Opinionated)

This rule tries to enforce naming patterns. In practice, it causes more arguments than it's worth.

{
  "rules": {
    "@typescript-eslint/naming-convention": "off"
  }
}
Enter fullscreen mode Exit fullscreen mode

Why: Naming is subjective. Let code review handle it.

4. no-console (Too Aggressive)

// ❌ Blocks legitimate debugging
console.log('User logged in:', userId);
console.error('Payment failed:', error);
Enter fullscreen mode Exit fullscreen mode

Better approach: Allow console, but remove before production

{
  "rules": {
    "no-console": "off" // Or use a build step to strip console.logs
  }
}
Enter fullscreen mode Exit fullscreen mode

5. @typescript-eslint/ban-ts-comment (Too Strict)

Sometimes you need @ts-ignore for legitimate reasons (working around library bugs, etc.)

{
  "rules": {
    "@typescript-eslint/ban-ts-comment": [
      "warn",
      {
        "ts-ignore": "allow-with-description",
        "minimumDescriptionLength": 10
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

This allows @ts-ignore but requires explanation:

// @ts-ignore: Library types are wrong, PR submitted to DefinitelyTyped
const result = libraryFunction(param);
Enter fullscreen mode Exit fullscreen mode

The Complete Configuration

Here's the full config that balances safety and DX:

{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2022,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    },
    "project": "./tsconfig.json"
  },
  "plugins": [
    "@typescript-eslint",
    "react",
    "react-hooks",
    "jsx-a11y"
  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:jsx-a11y/recommended",
    "prettier"
  ],
  "rules": {
    // === Tier 1: Bug Prevention (Always Error) ===
    "@typescript-eslint/no-floating-promises": "error",
    "@typescript-eslint/no-misused-promises": "error",
    "@typescript-eslint/await-thenable": "error",
    "@typescript-eslint/no-unnecessary-type-assertion": "error",
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "error",
    "@typescript-eslint/no-unused-vars": [
      "error",
      {
        "argsIgnorePattern": "^_",
        "varsIgnorePattern": "^_"
      }
    ],

    // === Tier 2: Quality Improvement (Warn) ===
    "@typescript-eslint/no-explicit-any": "warn",
    "@typescript-eslint/no-non-null-assertion": "warn",
    "@typescript-eslint/no-unsafe-assignment": "warn",
    "@typescript-eslint/no-unsafe-member-access": "warn",
    "@typescript-eslint/no-unsafe-call": "warn",
    "@typescript-eslint/prefer-nullish-coalescing": "warn",
    "@typescript-eslint/prefer-optional-chain": "warn",
    "@typescript-eslint/consistent-type-imports": [
      "warn",
      { "prefer": "type-imports" }
    ],

    // === Tier 3: Disable Annoying Rules ===
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/explicit-module-boundary-types": "off",
    "react/prop-types": "off",
    "react/react-in-jsx-scope": "off", // Not needed in React 17+
    "no-console": "off",

    // === Accessibility (Customize for your app) ===
    "jsx-a11y/no-autofocus": "off",
    "jsx-a11y/click-events-have-key-events": "warn"
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Performance Optimization

ESLint can be slow on large codebases. Optimize it:

1. Use Type-Aware Rules Selectively

{
  "parserOptions": {
    // Only use project for files that need type-aware rules
    "project": "./tsconfig.json"
  },
  "overrides": [
    {
      // Disable type-aware rules for test files
      "files": ["**/*.test.ts", "**/*.test.tsx"],
      "rules": {
        "@typescript-eslint/no-floating-promises": "off"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

2. Ignore Generated Files

// .eslintignore
node_modules/
dist/
build/
*.config.js
coverage/
.next/
Enter fullscreen mode Exit fullscreen mode

3. Use Caching

// package.json
{
  "scripts": {
    "lint": "eslint . --cache --cache-location .eslintcache"
  }
}
Enter fullscreen mode Exit fullscreen mode

Integration with Prettier

ESLint and Prettier should complement, not fight:

npm install --save-dev eslint-config-prettier
Enter fullscreen mode Exit fullscreen mode
{
  "extends": [
    // ... other extends
    "prettier" // MUST be last
  ]
}
Enter fullscreen mode Exit fullscreen mode

What Prettier handles:

  • Semicolons
  • Quotes
  • Indentation
  • Line length
  • Trailing commas

What ESLint handles:

  • Code quality
  • Bug prevention
  • Best practices

Never configure ESLint for formatting. Let Prettier do that.

Gradual Adoption Strategy

Don't enable all rules at once. Roll them out gradually:

Phase 1: Foundation (Week 1)

{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Phase 2: React Rules (Week 2-3)

{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Phase 3: Strict Type Safety (Week 4-6)

{
  "rules": {
    "@typescript-eslint/no-floating-promises": "error",
    "@typescript-eslint/no-misused-promises": "error"
  }
}
Enter fullscreen mode Exit fullscreen mode

Phase 4: Quality Rules (Week 7-8)

{
  "rules": {
    "@typescript-eslint/no-explicit-any": "warn",
    "@typescript-eslint/prefer-nullish-coalescing": "warn"
  }
}
Enter fullscreen mode Exit fullscreen mode

Phase 5: Accessibility (Week 9+)

{
  "extends": [
    "plugin:jsx-a11y/recommended"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Handling Existing Violations

When adding new rules to existing codebases:

Option 1: Fix Incrementally

{
  "rules": {
    "@typescript-eslint/no-explicit-any": "warn" // Start with warning
  }
}
Enter fullscreen mode Exit fullscreen mode

Fix warnings over time. Upgrade to "error" when all fixed.

Option 2: Grandfather Existing Code

{
  "overrides": [
    {
      "files": ["src/legacy/**/*"],
      "rules": {
        "@typescript-eslint/no-explicit-any": "off"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

New code follows rules. Legacy code exempted (for now).

Option 3: Set Max Warnings

// package.json
{
  "scripts": {
    "lint": "eslint . --max-warnings 100"
  }
}
Enter fullscreen mode Exit fullscreen mode

Allow existing warnings, but don't allow new ones.

VSCode Integration

Make ESLint fix issues on save:

// .vscode/settings.json
{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ]
}
Enter fullscreen mode Exit fullscreen mode

CI/CD Integration

Block PRs with lint errors:

# .github/workflows/lint.yml
name: Lint

on: [pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm ci
      - run: npm run lint
Enter fullscreen mode Exit fullscreen mode

Pro tip: Fail on errors, allow warnings:

{
  "scripts": {
    "lint": "eslint .",
    "lint:ci": "eslint . --max-warnings 0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Use lint locally (warnings OK), lint:ci in CI (no warnings).

Common Pitfalls

Pitfall 1: Too Many Rules at Once

Problem: Team gets overwhelmed, disables ESLint entirely.

Solution: Add rules gradually. Measure before/after.

Pitfall 2: Treating Warnings as Noise

Problem: 1000+ warnings. Team ignores them all.

Solution: Either fix warnings or make them errors. Warnings should be temporary.

Pitfall 3: Not Explaining Why

Problem: Team doesn't understand why rules exist.

Solution: Document your rules with links to examples of bugs they prevent.

Pitfall 4: Inconsistent Application

Problem: Rules enforced in CI but not locally.

Solution: Use git hooks to run ESLint pre-commit.

npm install --save-dev husky lint-staged
Enter fullscreen mode Exit fullscreen mode
// package.json
{
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Measuring Success

Track these metrics:

Before ESLint improvements:

  • Production bugs per week
  • Code review comments about bugs
  • Time spent debugging

After ESLint improvements:

  • Same metrics
  • ESLint violations caught
  • Developer satisfaction (survey)

Success looks like:

  • Fewer production bugs
  • Faster code reviews
  • Developers saying "ESLint caught this before I committed"

The Rules Summary

Always Enable (Error)

  • @typescript-eslint/no-floating-promises
  • @typescript-eslint/no-misused-promises
  • @typescript-eslint/await-thenable
  • react-hooks/rules-of-hooks
  • react-hooks/exhaustive-deps
  • @typescript-eslint/no-unused-vars

Usually Enable (Warn)

  • @typescript-eslint/no-explicit-any
  • @typescript-eslint/no-non-null-assertion
  • @typescript-eslint/prefer-nullish-coalescing
  • @typescript-eslint/prefer-optional-chain
  • @typescript-eslint/consistent-type-imports

Usually Disable

  • @typescript-eslint/explicit-function-return-type
  • react/prop-types
  • no-console
  • @typescript-eslint/naming-convention

Conclusion

Good ESLint configuration is about signal over noise.

The goal isn't to enforce style. The goal is to prevent bugs and improve code quality without annoying developers.

Start conservative:

  • Enable bug-preventing rules first
  • Add quality rules gradually
  • Disable annoying rules
  • Let Prettier handle formatting

Measure results:

  • Fewer bugs?
  • Faster reviews?
  • Happier developers?

If ESLint is catching bugs before they ship, it's working. If developers are disabling it, it's not.

The best ESLint config is the one your team actually uses.


What ESLint rules do you love or hate? Share your config in the comments!

Top comments (0)