TypeScript Linting and Static Analysis
TypeScript's type system catches many errors at compile time, but linting and static analysis tools enforce code quality, consistent style, and catch additional issues beyond type errors. A robust linting setup prevents bugs, improves maintainability, and ensures code consistency across the team.
Overview
While TypeScript's compiler ensures type safety, linting tools ensure code quality and consistency. The TypeScript compiler focuses on "will this code run correctly?" while linting focuses on "is this code maintainable, secure, and following best practices?"
The TypeScript quality stack:
- TypeScript compiler - Type checking and compilation
- ESLint - Code quality, patterns, and logical errors
- Prettier - Consistent code formatting
- Husky + lint-staged - Pre-commit enforcement
- CI/CD pipelines - Quality gates before merge
These tools should be configured in your project and enforced automatically in CI/CD pipelines. Manual code reviews should focus on architecture and logic, not formatting or style violations that tools catch automatically. See our Pipeline Configuration for CI/CD integration patterns.
ESLint for Code Quality
ESLint is the standard linting tool for TypeScript and JavaScript. It detects problematic patterns, enforces coding conventions, and catches potential bugs that the TypeScript compiler might miss. While TypeScript catches type errors, ESLint catches logical errors, unused code, and style violations.
Why ESLint Matters
The TypeScript compiler focuses on type correctness, not code quality. Consider this example:
// TypeScript compiler: No errors - types are correct
function processPayment(payment: Payment): void {
const result = fetchPaymentStatus(payment.id); // Returns Promise<Status>
console.log('Processing payment'); // Console statement in production code
}
// ESLint: Multiple issues detected
// 1. no-floating-promises: Promise not awaited or handled
// 2. no-console: Console statement should use logging framework
// 3. explicit-function-return-type: Async function should return Promise<void>
ESLint catches issues like:
- Unused variables - Often indicate bugs or incomplete refactoring
- Unreachable code - Dead code that should be removed
- Console statements - Should use proper logging in production
- Floating promises - Async operations without error handling
- Complexity violations - Functions that are too complex to maintain
Without ESLint, these issues only surface during code review (wasting reviewer time) or in production (causing bugs).
TypeScript-Specific ESLint
Use @typescript-eslint/parser and @typescript-eslint/eslint-plugin to enable TypeScript-aware linting. This provides rules specific to TypeScript like preferring interface over type for object shapes, enforcing consistent type assertions, and detecting misuse of TypeScript features.
Base configuration:
// .eslintrc.json
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"project": "./tsconfig.json" // Required for type-aware rules
},
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"rules": {
// TypeScript-specific rules
"@typescript-eslint/no-explicit-any": "error", // Ban 'any' type
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_", // Allow unused vars prefixed with _
"varsIgnorePattern": "^_"
}
],
"@typescript-eslint/explicit-function-return-type": [
"error",
{
"allowExpressions": true, // Allow inferred returns in arrow functions
"allowTypedFunctionExpressions": true
}
],
"@typescript-eslint/no-floating-promises": "error", // Must handle promises
"@typescript-eslint/await-thenable": "error", // Only await promises
"@typescript-eslint/no-misused-promises": "error", // Proper promise usage
"@typescript-eslint/strict-boolean-expressions": "warn", // Strict boolean checks
// General code quality
"no-console": "warn", // Prevent console.log in production
"no-debugger": "error", // No debugger statements
"no-alert": "error", // No alert() calls
"eqeqeq": ["error", "always"], // Require === and !==
"curly": ["error", "all"], // Require braces for all control structures
"no-var": "error", // Use let/const, not var
"prefer-const": "error", // Prefer const when variable not reassigned
"no-duplicate-imports": "error" // One import statement per module
}
}
Installation
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
# For React projects, also install:
npm install --save-dev eslint-plugin-react eslint-plugin-react-hooks
# For Angular projects, use Angular ESLint:
ng add @angular-eslint/schematics
Running ESLint
# Check all TypeScript files
npx eslint 'src/**/*.ts' 'src/**/*.tsx'
# Auto-fix fixable issues
npx eslint 'src/**/*.ts' --fix
# Check specific file
npx eslint src/services/payment-service.ts
# Use cache for faster repeated runs
npx eslint 'src/**/*.ts' --cache
Common Violations and Fixes
| Rule | Issue | Fix |
|---|---|---|
no-explicit-any | Using any type | Replace with proper type or unknown |
no-unused-vars | Unused variable | Remove variable or prefix with _ if intentionally unused |
no-floating-promises | Promise not awaited/handled | Add await or .catch() |
strict-boolean-expressions | Non-boolean in condition | Explicitly compare: if (value !== null) not if (value) |
explicit-function-return-type | Missing return type | Add explicit return type annotation |
Example fixes:
// BAD: Using 'any'
function process(data: any) {
return data.amount;
}
// GOOD: Proper type
function process(data: Payment) {
return data.amount;
}
// BAD: Unused variable
function calculate(amount: number, fee: number) {
const discount = 0.1; // ERROR: 'discount' is declared but never used
return amount + fee;
}
// GOOD: Remove unused variable
function calculate(amount: number, fee: number) {
return amount + fee;
}
// BAD: Floating promise
async function loadData() {
apiClient.fetchPayments(); // ERROR: Promise not awaited
}
// GOOD: Await promise
async function loadData() {
await apiClient.fetchPayments();
}
For React-specific ESLint rules, see our React Testing guidelines.
Prettier for Code Formatting
Prettier is an opinionated code formatter that automatically formats code to a consistent style. Unlike ESLint which focuses on code quality, Prettier handles purely stylistic concerns - indentation, line length, quote style, trailing commas, semicolons, etc.
Why Prettier
Manual formatting wastes time and creates noise in code reviews:
- Developers spend time manually formatting code
- Code reviews include comments like "add a space here" or "wrong indentation"
- Diffs are polluted with formatting changes unrelated to logic
- Different developers format code differently, creating inconsistency
Prettier eliminates these problems by enforcing a consistent, non-configurable format. It integrates with editors to format-on-save and can be enforced in CI to reject incorrectly formatted code.
Prettier vs ESLint
Prettier: Handles formatting (whitespace, quotes, semicolons, line breaks) ESLint: Handles code quality (logic errors, best practices, unused code)
Use both together - Prettier for formatting, ESLint for quality. Some ESLint rules conflict with Prettier (like quotes, semi, indent). Disable these using eslint-config-prettier.
Configuration
// .prettierrc
{
"semi": true, // Require semicolons
"trailingComma": "es5", // Trailing commas where valid in ES5
"singleQuote": true, // Use single quotes
"printWidth": 100, // Max line length
"tabWidth": 2, // 2 space indentation
"useTabs": false, // Spaces, not tabs
"arrowParens": "always", // Always parentheses in arrow functions
"endOfLine": "lf" // Unix line endings
}
# .prettierignore
# Build outputs
dist/
build/
coverage/
# Dependencies
node_modules/
# Generated code
src/generated/
# Lock files
package-lock.json
yarn.lock
Installation
npm install --save-dev prettier eslint-config-prettier
# Install Prettier plugin for ESLint integration
npm install --save-dev eslint-plugin-prettier
Update ESLint config to use Prettier:
// .eslintrc.json
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:prettier/recommended" // Must be last - disables conflicting rules
]
}
Running Prettier
# Check formatting
npx prettier --check 'src/**/*.ts' 'src/**/*.tsx'
# Format files
npx prettier --write 'src/**/*.ts' 'src/**/*.tsx'
# Format all supported files
npx prettier --write .
# Check specific file
npx prettier --check src/services/payment-service.ts
Editor Integration
Configure your IDE to run Prettier on save. This provides instant formatting feedback and ensures code is always formatted correctly.
VS Code (.vscode/settings.json):
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
IntelliJ/WebStorm: Settings → Languages & Frameworks → JavaScript → Prettier → Enable "On save"
See our IDE Setup guide for detailed editor configuration.
Type-Aware Linting
TypeScript ESLint supports type-aware rules that leverage TypeScript's type checker for deeper analysis. These rules catch issues that require understanding types, like using floating promises, awaiting non-promises, or returning promises from void functions.
Enabling Type-Aware Rules
Set parserOptions.project in ESLint config to point to your tsconfig.json. This allows ESLint to access type information but makes linting slower - only enable for comprehensive CI checks, not fast pre-commit hooks.
// .eslintrc.json
{
"parserOptions": {
"project": "./tsconfig.json" // Enables type-aware rules
},
"rules": {
// These rules require type information
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unsafe-return": "error",
"@typescript-eslint/require-await": "warn",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/prefer-optional-chain": "error"
}
}
Type-Aware Rules in Action
// no-floating-promises - Promise not awaited
async function fetchData() {
apiClient.get('/data'); // ERROR: Promise not awaited or handled
}
// Fix: Await the promise
async function fetchData() {
await apiClient.get('/data');
}
// await-thenable - Awaiting non-promise
async function process(value: number) {
await value; // ERROR: 'number' is not a promise
}
// Fix: Don't await non-promises
async function process(value: number) {
return value;
}
// no-unsafe-member-access - Accessing member on 'any'
function process(data: any) {
return data.amount; // ERROR: Unsafe member access on 'any' type
}
// Fix: Properly type the parameter
function process(data: Payment) {
return data.amount;
}
// no-unnecessary-type-assertion - Type assertion not needed
const amount: number = payment.amount as number; // ERROR: Unnecessary assertion
// Fix: Remove unnecessary assertion
const amount: number = payment.amount;
// prefer-nullish-coalescing - Should use ?? instead of ||
const name = user.name || 'Unknown'; // ERROR: '' and 0 are falsy but valid
// Fix: Use nullish coalescing
const name = user.name ?? 'Unknown'; // Only null/undefined trigger default
Performance Considerations
Type-aware rules significantly increase linting time (they require TypeScript compilation). For a typical project:
- Without type-aware rules: 2-5 seconds
- With type-aware rules: 10-30 seconds
Strategy: Use type-aware rules in CI for comprehensive checks, but consider disabling them in pre-commit hooks for faster feedback.
// .eslintrc.precommit.json (fast config for pre-commit)
{
"extends": ["./.eslintrc.json"],
"parserOptions": {
// Don't load project for faster linting
},
"rules": {
// Disable slow type-aware rules
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-misused-promises": "off"
}
}
Pre-commit Hooks with Husky and lint-staged
Pre-commit hooks run linting and formatting checks before commits, providing immediate feedback and preventing incorrectly formatted or low-quality code from entering the repository. This catches issues early, before they reach CI.
Why Pre-commit Hooks
Without pre-commit hooks:
- Developer writes code
- Developer commits code
- Developer pushes code
- CI runs, finds linting errors
- PR is blocked
- Developer fixes errors, pushes again
- Total time: 10-15 minutes
With pre-commit hooks:
- Developer writes code
- Pre-commit hook runs, finds errors
- Developer fixes errors
- Commit succeeds
- Total time: 2-3 minutes
Pre-commit hooks provide instant feedback, preventing the commit-push-wait-fix cycle.
Husky and lint-staged
Husky: Manages Git hooks in npm projects lint-staged: Runs linters only on staged files (files about to be committed)
Running linters only on staged files makes pre-commit checks fast - you only check files you're changing, not the entire codebase.
Configuration
// package.json
{
"scripts": {
"prepare": "husky install", // Install hooks automatically on npm install
"lint": "eslint 'src/**/*.{ts,tsx}'",
"lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix",
"format": "prettier --write 'src/**/*.{ts,tsx,json,md}'",
"format:check": "prettier --check 'src/**/*.{ts,tsx,json,md}'",
"type-check": "tsc --noEmit"
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix", // Auto-fix ESLint issues
"prettier --write" // Format with Prettier
],
"*.{json,md}": [
"prettier --write" // Format non-TS files
]
}
}
Installation
npm install --save-dev husky lint-staged
# Initialize Husky (creates .husky directory)
npx husky install
# Create pre-commit hook
npx husky add .husky/pre-commit "npx lint-staged"
How It Works
- Developer stages files for commit (
git add file.ts) - Developer runs
git commit -m "message" - Pre-commit hook triggers (
.husky/pre-commit) lint-stagedruns ESLint and Prettier on only staged*.tsand*.tsxfiles- If checks pass, commit proceeds; if they fail, commit is blocked
- Developer sees errors, fixes them, and re-commits
# Example output on commit
$ git commit -m "Add payment processing"
Preparing lint-staged...
Running tasks for staged files...
❯ *.{ts,tsx} - 3 files
eslint --fix
prettier --write
Applying modifications from tasks...
Cleaning up temporary files...
[main abc1234] Add payment processing
3 files changed, 45 insertions(+)
Bypassing Hooks (Use Sparingly)
Sometimes you need to commit work-in-progress code that doesn't pass linting:
# Skip pre-commit hooks (use only when necessary)
git commit --no-verify -m "WIP: Incomplete feature"
Warning: Always fix linting errors before pushing. CI will catch them anyway, and bypassing hooks regularly indicates your linting rules are too strict or your workflow needs adjustment.
For teams that find pre-commit checks too restrictive (blocking commits during exploratory work), run checks in CI instead and fail pull requests - see our Git Workflow guide for trade-offs.
CI/CD Integration
Linting must run in CI/CD pipelines to enforce quality gates. Even with pre-commit hooks, developers can bypass them (git commit --no-verify), so CI enforcement is essential. Configure pipelines to fail when linting errors are detected.
GitHub Actions Example
# .github/workflows/quality.yml
name: Code Quality
on: [pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Check Prettier formatting
run: npm run format:check
- name: Type check
run: npm run type-check # npx tsc --noEmit
GitLab CI Example
# .gitlab-ci.yml
quality:
stage: test
image: node:18
script:
- npm ci
- npm run lint
- npm run format:check
- npm run type-check
cache:
paths:
- node_modules/
allow_failure: false # Fail pipeline on errors
Caching for Faster CI
Cache node_modules and ESLint cache to speed up repeated runs:
# GitHub Actions with caching
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm' # Caches node_modules automatically
- name: Run ESLint with cache
run: npx eslint 'src/**/*.ts' --cache --cache-location .eslintcache
- name: Cache ESLint results
uses: actions/cache@v3
with:
path: .eslintcache
key: eslint-${{ hashFiles('**/*.ts', '**/*.tsx') }}
Most CI systems support caching dependencies, dramatically reducing build times from 2-3 minutes to 30-60 seconds on repeated runs.
See our CI Testing Best Practices for comprehensive CI configuration patterns.
Auto-fix vs Manual Fixes
Some linting violations can be automatically fixed (--fix flag), while others require manual intervention. Understanding which is which helps you work efficiently.
Auto-fixable Issues
These can be fixed automatically and should be handled by pre-commit hooks:
- Code formatting - All Prettier rules (indentation, quotes, semicolons)
- Import sorting - Alphabetical ordering of imports
- Unused imports - Remove unused import statements
- Prefer const - Change
lettoconstwhen variable isn't reassigned - Quote style - Consistent single or double quotes
- Trailing commas - Add/remove trailing commas for consistency
# Auto-fix everything that can be fixed
npx eslint --fix 'src/**/*.ts'
npx prettier --write 'src/**/*.ts'
Manual Fixes Required
These require developer judgment and cannot be auto-fixed:
- Using
anytype - Need to determine proper type - Logic errors - Unreachable code, incorrect comparisons
- Missing return types - Need to specify return type
- Floating promises - Need to add
awaitor error handling - Complex refactorings - Breaking up long functions, extracting methods
- Architectural violations - Fixing import cycles, layer violations
# Show remaining errors after auto-fix
npx eslint 'src/**/*.ts'
Workflow Example
# 1. Auto-fix what you can
npx eslint --fix 'src/**/*.ts'
npx prettier --write 'src/**/*.ts'
# 2. Check remaining issues
npx eslint 'src/**/*.ts'
# Output:
# src/services/payment.ts
# 12:5 error Missing return type on function @typescript-eslint/explicit-function-return-type
# 23:3 error Promise returned in function with void return type @typescript-eslint/no-misused-promises
# 3. Manually fix remaining issues
# - Add return type to function at line 12
# - Make function async or change return type at line 23
# 4. Verify all issues resolved
npx eslint 'src/**/*.ts'
# No errors!
Reviewing Auto-fixed Changes
While auto-fix is convenient, always review changes before committing. Occasionally, auto-fix makes unintended changes that reduce readability.
# After auto-fixing, review changes
git diff
# Example: Line wrapping might reduce readability
# Before (readable):
const payment = createPayment(user, amount, currency);
# After auto-fix (less readable but meets line length limit):
const payment = createPayment(
user,
amount,
currency
);
If auto-fix consistently produces poor formatting in specific cases, adjust your Prettier config (e.g., increase printWidth) or disable the rule for that file with a comment.
Custom ESLint Rules
For team-specific or domain-specific standards, create custom ESLint rules. This enforces architectural patterns, prevents common mistakes specific to your codebase, and codifies team conventions.
When to Create Custom Rules
Create custom rules for patterns that:
- Recur frequently in code reviews (automate the feedback)
- Cause production bugs (prevent them automatically)
- Violate architectural constraints (enforce your architecture)
- Are specific to your domain (e.g., payment processing patterns)
Don't create custom rules for:
- One-off issues easily caught in code review
- Patterns that existing rules already cover
- Overly specific edge cases that rarely occur
Custom Rule Example
// eslint-rules/no-direct-state-mutation.ts
import { ESLintUtils } from '@typescript-eslint/utils';
export const rule = ESLintUtils.RuleCreator(
(name) => `https://your-docs.com/rules/${name}`
)({
name: 'no-direct-state-mutation',
meta: {
type: 'problem',
docs: {
description: 'Disallow direct state mutations outside setState',
recommended: 'error',
},
messages: {
directMutation: 'Direct state mutation detected. Use setState instead.',
},
schema: [],
},
defaultOptions: [],
create(context) {
return {
AssignmentExpression(node) {
// Detect patterns like: this.state.value = ...
if (
node.left.type === 'MemberExpression' &&
node.left.object.type === 'MemberExpression' &&
node.left.object.property.type === 'Identifier' &&
node.left.object.property.name === 'state'
) {
context.report({
node,
messageId: 'directMutation',
});
}
},
};
},
});
Using Custom Rules
// .eslintrc.json
{
"plugins": ["./eslint-rules"],
"rules": {
"custom/no-direct-state-mutation": "error"
}
}
Examples of Custom Rules
- Architectural: Enforce layering (services can't import from controllers)
- Domain-specific: Payment amounts must be strings, not numbers (to avoid precision issues)
- Security: Sensitive data must not be logged
- Testing: Test file names must match source file names
- API conventions: All API calls must use the centralized API client
Custom rules should be:
- Documented: Clear explanation of why the rule exists
- Team-approved: Reviewed and agreed upon by the team
- Maintainable: Simple enough that future developers can understand and modify
Only create custom rules for patterns that genuinely improve code quality and prevent real problems - avoid over-engineering with too many custom rules.
Tool Comparison
Understanding what each tool does helps you configure them effectively:
| Tool | Purpose | Auto-fix | Type-aware | Speed | Integration |
|---|---|---|---|---|---|
| TypeScript compiler | Type checking | No | Yes (built-in) | Medium | Always on |
| ESLint | Code quality, patterns | Some rules | With config | Fast-Medium | Pre-commit, CI |
| Prettier | Code formatting | Yes (all) | No | Very Fast | Pre-commit, CI, IDE |
| Husky + lint-staged | Pre-commit hooks | N/A | N/A | Fast (staged only) | Pre-commit |
Recommended Setup
For most projects:
- TypeScript strict mode - Catches type errors (
"strict": truein tsconfig.json) - ESLint with @typescript-eslint - Catches code quality issues
- Prettier - Handles formatting
- Husky + lint-staged - Pre-commit checks
- CI enforcement - Quality gates in pipeline
For very large codebases:
- Disable type-aware ESLint rules in pre-commit hooks (too slow)
- Run full type-aware lint only in CI
- Use ESLint cache (
--cacheflag) for faster repeated runs - Consider incremental type checking in CI
Minimal vs Comprehensive Setup
Minimal (good for small teams starting out):
{
"devDependencies": {
"eslint": "^8.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"prettier": "^3.0.0"
}
}
Comprehensive (recommended for production applications):
{
"devDependencies": {
"eslint": "^8.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"prettier": "^3.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"husky": "^8.0.0",
"lint-staged": "^15.0.0"
}
}
Further Reading
Internal Documentation
- TypeScript General Guidelines - Core TypeScript best practices
- TypeScript Types - Advanced type patterns
- TypeScript Testing - Testing strategies
- React Testing - React-specific linting
- Angular Testing - Angular-specific linting
- CI/CD Pipeline Configuration - Pipeline integration
- IDE Setup - IDE configuration
External Resources
- ESLint Documentation
- TypeScript ESLint
- Prettier Documentation
- Husky Documentation
- lint-staged Documentation
Summary
Key Takeaways
- TypeScript + ESLint + Prettier - Use all three together for comprehensive quality
- Pre-commit hooks - Catch issues before they enter the repository
- CI enforcement - Quality gates prevent low-quality code from merging
- Type-aware rules - Enable in CI for deep analysis, disable in pre-commit for speed
- Auto-fix liberally - Let tools fix formatting automatically
- Manual fixes carefully - Developer judgment required for logic issues
- Custom rules sparingly - Only for recurring team-specific patterns
- Editor integration - Format-on-save provides instant feedback
- Cache for performance - Use ESLint cache and CI caching for speed
- Progressive adoption - Start minimal, add tools as team matures
Next Steps: Configure these tools in your project, integrate them into CI/CD pipelines, and review the TypeScript Testing guide for testing strategies.