Strict Configuration · Additional notes

1 min read
Mid-level1 min read
Rapid overview

Additional notes

Production-Ready Strict Setup

This guide covers configuring ESLint with strict, best-practice rules for professional TypeScript/React projects.


Complete Strict Configuration

Flat Config (ESLint 9+)

// eslint.config.js
import js from '@eslint/js';
import typescript from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import importPlugin from 'eslint-plugin-import';
import prettier from 'eslint-config-prettier';

export default typescript.config(
  // Base recommended configs
  js.configs.recommended,
  ...typescript.configs.strictTypeChecked,
  ...typescript.configs.stylisticTypeChecked,

  // Global ignores
  {
    ignores: ['dist/**', 'node_modules/**', 'coverage/**', '*.config.js'],
  },

  // TypeScript files
  {
    files: ['**/*.ts', '**/*.tsx'],
    languageOptions: {
      parserOptions: {
        project: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    plugins: {
      react,
      'react-hooks': reactHooks,
      import: importPlugin,
    },
    settings: {
      react: {
        version: 'detect',
      },
      'import/resolver': {
        typescript: true,
        node: true,
      },
    },
    rules: {
      // === TypeScript Strict Rules ===
      '@typescript-eslint/no-explicit-any': 'error',
      '@typescript-eslint/no-unused-vars': ['error', {
        argsIgnorePattern: '^_',
        varsIgnorePattern: '^_',
      }],
      '@typescript-eslint/explicit-function-return-type': ['error', {
        allowExpressions: true,
        allowTypedFunctionExpressions: true,
      }],
      '@typescript-eslint/explicit-module-boundary-types': 'error',
      '@typescript-eslint/no-non-null-assertion': 'error',
      '@typescript-eslint/strict-boolean-expressions': ['error', {
        allowString: false,
        allowNumber: false,
        allowNullableObject: true,
      }],
      '@typescript-eslint/no-floating-promises': 'error',
      '@typescript-eslint/no-misused-promises': 'error',
      '@typescript-eslint/await-thenable': 'error',
      '@typescript-eslint/no-unnecessary-condition': 'error',
      '@typescript-eslint/prefer-nullish-coalescing': 'error',
      '@typescript-eslint/prefer-optional-chain': 'error',
      '@typescript-eslint/consistent-type-imports': ['error', {
        prefer: 'type-imports',
        fixStyle: 'inline-type-imports',
      }],
      '@typescript-eslint/consistent-type-exports': 'error',
      '@typescript-eslint/no-import-type-side-effects': 'error',
      '@typescript-eslint/naming-convention': [
        'error',
        { selector: 'interface', format: ['PascalCase'] },
        { selector: 'typeAlias', format: ['PascalCase'] },
        { selector: 'enum', format: ['PascalCase'] },
        { selector: 'enumMember', format: ['UPPER_CASE'] },
        { selector: 'variable', format: ['camelCase', 'UPPER_CASE', 'PascalCase'] },
        { selector: 'function', format: ['camelCase', 'PascalCase'] },
        { selector: 'parameter', format: ['camelCase'], leadingUnderscore: 'allow' },
        { selector: 'property', format: ['camelCase', 'UPPER_CASE'] },
        { selector: 'method', format: ['camelCase'] },
      ],

      // === JavaScript Best Practices ===
      'no-console': ['error', { allow: ['warn', 'error'] }],
      'no-debugger': 'error',
      'no-alert': 'error',
      'no-var': 'error',
      'prefer-const': 'error',
      'prefer-arrow-callback': 'error',
      'prefer-template': 'error',
      'no-param-reassign': ['error', { props: true }],
      'no-nested-ternary': 'error',
      'no-unneeded-ternary': 'error',
      'no-else-return': ['error', { allowElseIf: false }],
      'no-return-await': 'error',
      'require-await': 'error',
      'no-await-in-loop': 'warn',
      'no-promise-executor-return': 'error',
      'prefer-promise-reject-errors': 'error',
      'no-throw-literal': 'error',
      'eqeqeq': ['error', 'always'],
      'curly': ['error', 'all'],
      'default-case': 'error',
      'default-case-last': 'error',
      'no-fallthrough': 'error',
      'no-implicit-coercion': 'error',
      'no-magic-numbers': ['warn', {
        ignore: [-1, 0, 1, 2],
        ignoreArrayIndexes: true,
        enforceConst: true,
      }],
      'complexity': ['warn', { max: 10 }],
      'max-depth': ['warn', { max: 3 }],
      'max-lines-per-function': ['warn', { max: 50, skipBlankLines: true, skipComments: true }],
      'max-params': ['warn', { max: 4 }],

      // === React Rules ===
      'react/jsx-uses-react': 'off', // Not needed with React 17+
      'react/react-in-jsx-scope': 'off',
      'react/prop-types': 'off', // Using TypeScript
      'react/jsx-no-target-blank': 'error',
      'react/jsx-no-useless-fragment': 'error',
      'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never' }],
      'react/self-closing-comp': 'error',
      'react/jsx-boolean-value': ['error', 'never'],
      'react/jsx-pascal-case': 'error',
      'react/no-array-index-key': 'warn',
      'react/no-danger': 'error',
      'react/no-unstable-nested-components': 'error',
      'react/hook-use-state': 'error',

      // === React Hooks Rules ===
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'error',

      // === Import Rules ===
      'import/order': ['error', {
        groups: [
          'builtin',
          'external',
          'internal',
          ['parent', 'sibling'],
          'index',
          'type',
        ],
        'newlines-between': 'always',
        alphabetize: { order: 'asc', caseInsensitive: true },
      }],
      'import/no-duplicates': 'error',
      'import/no-cycle': 'error',
      'import/no-self-import': 'error',
      'import/no-useless-path-segments': 'error',
      'import/first': 'error',
      'import/newline-after-import': 'error',
      'import/no-default-export': 'warn', // Prefer named exports
    },
  },

  // Test files - relaxed rules
  {
    files: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'],
    rules: {
      '@typescript-eslint/no-explicit-any': 'off',
      '@typescript-eslint/no-non-null-assertion': 'off',
      'no-magic-numbers': 'off',
      'max-lines-per-function': 'off',
    },
  },

  // Prettier must be last
  prettier,
);

Rule-by-Rule Breakdown

Type Safety Rules

// Disallow any - forces proper typing
'@typescript-eslint/no-explicit-any': 'error'

// Bad
function process(data: any) { }

// Good
function process(data: UserData) { }
// Require explicit return types on exported functions
'@typescript-eslint/explicit-module-boundary-types': 'error'

// Bad
export function getUser(id: string) {
  return db.users.find(id);
}

// Good
export function getUser(id: string): Promise<User | null> {
  return db.users.find(id);
}
// Prevent non-null assertions (!)
'@typescript-eslint/no-non-null-assertion': 'error'

// Bad
const name = user!.name;

// Good
const name = user?.name ?? 'Unknown';
// Strict boolean expressions
'@typescript-eslint/strict-boolean-expressions': 'error'

// Bad - truthy/falsy check
if (value) { }

// Good - explicit check
if (value !== null && value !== undefined) { }
if (value.length > 0) { }
if (value === true) { }

Async/Promise Safety

// Catch floating promises
'@typescript-eslint/no-floating-promises': 'error'

// Bad - unhandled promise
fetchData();

// Good
await fetchData();
// or
void fetchData(); // Explicit ignore
// or
fetchData().catch(handleError);
// Prevent misused promises
'@typescript-eslint/no-misused-promises': 'error'

// Bad - promise in condition
if (fetchData()) { }

// Good
if (await fetchData()) { }

Code Quality Rules

// Complexity limit
'complexity': ['warn', { max: 10 }]

// Function nesting depth
'max-depth': ['warn', { max: 3 }]

// Max function lines
'max-lines-per-function': ['warn', { max: 50 }]

// Max parameters
'max-params': ['warn', { max: 4 }]

// These encourage:
// - Smaller, focused functions
// - Early returns
// - Breaking complex logic into helpers

Import Organization

'import/order': ['error', {
  groups: ['builtin', 'external', 'internal', ['parent', 'sibling'], 'index', 'type'],
  'newlines-between': 'always',
  alphabetize: { order: 'asc' },
}]

// Results in:
import fs from 'fs';                    // builtin

import React from 'react';              // external
import { useQuery } from 'react-query';

import { config } from '@/config';      // internal (aliased)

import { Button } from '../Button';     // parent
import { Input } from './Input';        // sibling

import styles from './styles.css';      // index

import type { User } from '@/types';    // type imports last

Environment-Specific Configs

Development vs Production

// eslint.config.js
const isProd = process.env.NODE_ENV === 'production';

export default [
  {
    rules: {
      'no-console': isProd ? 'error' : 'warn',
      'no-debugger': isProd ? 'error' : 'warn',
      '@typescript-eslint/no-explicit-any': isProd ? 'error' : 'warn',
    },
  },
];

Framework-Specific: Next.js

import nextPlugin from '@next/eslint-plugin-next';

export default [
  // ... base config
  {
    plugins: {
      '@next/next': nextPlugin,
    },
    rules: {
      ...nextPlugin.configs.recommended.rules,
      ...nextPlugin.configs['core-web-vitals'].rules,
      'import/no-default-export': 'off', // Next.js requires default exports
    },
  },
  {
    files: ['**/page.tsx', '**/layout.tsx', '**/route.ts'],
    rules: {
      'import/no-default-export': 'off',
    },
  },
];

Framework-Specific: Angular

import angular from '@angular-eslint/eslint-plugin';
import angularTemplate from '@angular-eslint/eslint-plugin-template';

export default [
  {
    files: ['**/*.ts'],
    plugins: {
      '@angular-eslint': angular,
    },
    rules: {
      '@angular-eslint/component-class-suffix': 'error',
      '@angular-eslint/directive-class-suffix': 'error',
      '@angular-eslint/no-empty-lifecycle-method': 'error',
      '@angular-eslint/prefer-on-push-component-change-detection': 'warn',
    },
  },
  {
    files: ['**/*.html'],
    plugins: {
      '@angular-eslint/template': angularTemplate,
    },
    rules: {
      '@angular-eslint/template/no-negated-async': 'error',
      '@angular-eslint/template/use-track-by-function': 'warn',
    },
  },
];

CI/CD Integration

GitHub Actions

name: Lint

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci

      - name: Run ESLint
        run: npm run lint -- --max-warnings 0

      - name: Check types
        run: npm run type-check

Pre-push Hook

#!/bin/sh
# .husky/pre-push
npm run lint -- --max-warnings 0
npm run type-check

Gradual Strictness Adoption

Phase 1: Warnings

rules: {
  '@typescript-eslint/no-explicit-any': 'warn',
  '@typescript-eslint/strict-boolean-expressions': 'warn',
  'complexity': 'warn',
}

Phase 2: Errors for New Code

rules: {
  '@typescript-eslint/no-explicit-any': 'error',
  // ... but allow in legacy files
}

// .eslintignore-legacy
src/legacy/**

Phase 3: Full Strictness

Remove legacy ignores and enforce everywhere.


See also