Strict Configuration
6 min readRapid overview
- ESLint Strict Configuration
- Production-Ready Strict Setup
- Complete Strict Configuration
- Flat Config (ESLint 9+)
- Rule-by-Rule Breakdown
- Type Safety Rules
- Async/Promise Safety
- Code Quality Rules
- Import Organization
- Auto-Fix Examples
- What `eslint --fix` Can Fix
- Rules with Auto-Fix
- Rules That Cannot Auto-Fix
- Environment-Specific Configs
- Development vs Production
- Framework-Specific: Next.js
- Framework-Specific: Angular
- CI/CD Integration
- GitHub Actions
- Pre-push Hook
- Gradual Strictness Adoption
- Phase 1: Warnings
- Phase 2: Errors for New Code
- Phase 3: Full Strictness
- Interview Questions
- 1. How do you introduce strict linting to an existing codebase?
- 2. Which rules have the highest impact on code quality?
- 3. How do you handle rules that conflict with your codebase style?
ESLint Strict Configuration
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
Auto-Fix Examples
What eslint --fix Can Fix
// Before
var x = 1;
const y = "hello";
if(x == 1){ console.log(y) }
const fn = function() { return 1; };
// After --fix
const x = 1;
const y = 'hello';
if (x === 1) {
console.log(y);
}
const fn = () => 1;
Rules with Auto-Fix
| Rule | Fixes |
|---|---|
no-var | var → let/const |
prefer-const | let → const when never reassigned |
prefer-arrow-callback | Function expressions → arrow functions |
prefer-template | String concatenation → template literals |
eqeqeq | == → === |
quotes | Quote style normalization |
semi | Add/remove semicolons |
import/order | Reorder imports |
@typescript-eslint/consistent-type-imports | Add type keyword |
Rules That Cannot Auto-Fix
no-explicit-any- Requires manual typingno-unused-vars- Requires removing or usingcomplexity- Requires refactoringno-floating-promises- Requires await/catch/void
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.
Interview Questions
1. How do you introduce strict linting to an existing codebase?
- Start with warnings, not errors
- Use
--max-warningsto prevent new issues - Fix incrementally by directory/feature
- Use git hooks to prevent new violations
- Track metrics (violation count over time)
2. Which rules have the highest impact on code quality?
@typescript-eslint/no-explicit-any- Forces proper typing@typescript-eslint/strict-boolean-expressions- Prevents truthy/falsy bugs@typescript-eslint/no-floating-promises- Catches unhandled asynccomplexity- Limits function complexityimport/no-cycle- Prevents circular dependencies
3. How do you handle rules that conflict with your codebase style?
- Customize rule options (many rules have configuration)
- Use overrides for specific file patterns
- Disable with inline comments sparingly (with justification)
- Create team consensus on which rules to modify