Integrating ESLint with TypeScript Projects (Specific Rules)
Introduction
ESLint is the de facto linter for JavaScript projects, but when you introduce TypeScript, the landscape changes: you gain type-safety and richer AST information, which lets you enforce more precise rules. However, configuring ESLint to leverage TypeScript correctly—so rules don't conflict with the compiler, so autofixes are safe, and so your team gets meaningful warnings—takes care. In this article, designed for intermediate developers, you'll learn how to integrate ESLint with TypeScript projects focused on specific rules: how to pick parser settings, which rules to prefer from @typescript-eslint, how to configure rule options for real-world codebases, and how to align linter rules with tsconfig options.
We'll cover parser options and tsconfig interactions, concrete rule-by-rule guidance (including rule rationale and code examples), techniques for rule autofixes, performance tips for large projects and monorepos, and troubleshooting strategies. You will also get advanced techniques for using rules with project references and isolatedModules and how to handle declaration files and JS interop during linting. Throughout the tutorial you'll find actionable configuration snippets and examples you can copy-paste into your own projects.
By the end of this guide you'll be able to: pick and tune TypeScript-aware ESLint rules, avoid common conflicts between TypeScript compiler settings and linter rules, integrate automatic fixes responsibly, and maintain a consistent rule set across small and large codebases.
Background & Context
ESLint operates on the program's AST. For TypeScript, you should use @typescript-eslint/parser and the rules from @typescript-eslint to access type-aware linting. Unlike plain JS linting, TypeScript linting can use type information to catch issues like unsafe any usage, unreachable types, or inconsistent public API typing. The TypeScript compiler (tsc) and ESLint have overlapping responsibilities—tsc enforces type correctness while ESLint enforces style and patterns. Good integration minimizes duplicated checks, avoids false positives, and lets each tool play to its strengths.
A correct integration also considers tsconfig.json options (for example understanding tsconfig categories), module interop flags, and generation of declarations. Many teams use ESLint to enforce typing patterns for public APIs and internal code—making rules like explicit-function-return-type or consistent-type-definitions valuable. We'll tie rule choices to compiler settings like strictNullChecks and isolatedModules so your lint rules remain actionable.
Key Takeaways
- Use @typescript-eslint/parser with project-aware parserOptions for type-aware rules
- Prefer @typescript-eslint versions of rules over base ESLint rules when available
- Sync lint rules with tsconfig.json (e.g., strictNullChecks, isolatedModules, esModuleInterop)
- Configure critical rules: no-explicit-any, no-unused-vars, explicit-module-boundary-types, consistent-type-definitions
- Use autofix carefully; run fixes in CI or via pre-commit hooks
- Optimize performance for large projects and monorepos with cache and selective project configs
Prerequisites & Setup
Before you start, ensure you have:
- Node.js (12+ recommended) and npm or yarn
- A TypeScript project with a valid tsconfig.json
- ESLint v7+ or v8+ and @typescript-eslint packages
Install the basic packages:
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
If you use Prettier, add eslint-config-prettier and eslint-plugin-prettier. If your project spans multiple TS projects (project references) you will configure parserOptions.project to include them. See the later sections for a recommended minimal .eslintrc.js sample that you can adapt.
Main Tutorial Sections
1) Choosing the right parserOptions and tsconfig interplay
To enable type-aware rules, you must configure @typescript-eslint/parser with parserOptions.project pointing to your tsconfig.json. Example:
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
};Use project-aware mode only if you need rules requiring type information (e.g., no-unsafe-assignment). If your project grows into multiple tsconfigs (monorepos), list them all: project: ['./tsconfig.json','packages/*/tsconfig.json']. For an explanation of compiler option categories and how they affect linting and tooling, refer to Understanding tsconfig.json compiler option categories.
2) Prefer @typescript-eslint rules over base ESLint rules
Many base ESLint rules conflict with TypeScript syntax (e.g., no-unused-vars). Instead enable the @typescript-eslint variant and disable the base rule:
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
}This avoids false positives for types and interface-only imports. Similarly replace rules like no-shadow, no-undef, and consistent-return with their TypeScript-aware implementations. This approach reduces friction and makes errors meaningful for TypeScript code.
3) Rule: no-explicit-any — strategy and examples
One popular rule is @typescript-eslint/no-explicit-any. Blanket banning 'any' can slow development; tune the rule with allowances and targeted severity:
'@typescript-eslint/no-explicit-any': ['warn', { ignoreRestArgs: true }]Use 'warn' to start, and pair with @typescript-eslint/explicit-module-boundary-types to ensure public APIs don't leak any. When migrating large codebases, use file-level or line-level suppression as a temporary measure, and progressively remove them.
Example:
// bad
function parse(body: any) { /* ... */ }
// better
function parse(body: unknown) {
if (typeof body === 'string') { /* ... */ }
}4) Rule: explicit-module-boundary-types and API design
@typescript-eslint/explicit-module-boundary-types enforces declaring types for exported functions and methods. This becomes a guardrail for public API stability. Configure it per your team style:
'@typescript-eslint/explicit-module-boundary-types': ['warn', {
allowArgumentsExplicitlyTypedAsAny: false
}],If you use this rule, pair it with a strategy to keep declarations concise: prefer interface or type aliases for complex shapes and consistent-type-definitions (see next section). This rule is especially valuable for libraries; if you auto-generate declaration files, ensure lint rules align with declaration output—see guidance on generating declaration files automatically.
5) Rule: consistent-type-definitions and team conventions
Decide whether to use type or interface for object shapes. Configure the rule to enforce a single style:
'@typescript-eslint/consistent-type-definitions': ['error', 'interface']
Interfaces are better for public, extendable APIs and some tooling; types are powerful for unions. Enforcing a single style reduces churn in diffs and code reviews. When evaluating this decision, consider project scale—larger codebases benefit from consistent patterns noted in guides like Best Practices for Structuring Large TypeScript Projects.
6) Rule: strict-boolean-expressions and null-safety
Rules like @typescript-eslint/strict-boolean-expressions help you avoid surprising coercions. This rule interacts with TypeScript's strictNullChecks. If your tsconfig has strictNullChecks: true (recommended), enable the linter rule as follows:
'@typescript-eslint/strict-boolean-expressions': ['error', { allowNullable: false }]If you haven't enabled strictNullChecks yet, follow a migration plan (incrementally enabling checks) described in Configuring strictNullChecks in TypeScript: A Practical Guide for Intermediate Developers.
7) Dealing with isolatedModules and transpilation-only builds
If you rely on tools like Babel or transpile-only pipelines, the isolatedModules compiler option (and the linter's expectations) matter. Many ESLint rules assume type information that isn't available in isolated module scenarios. For those setups, prefer rules that don't require parserOptions.project or configure separate eslintrc for transpiled packages. See deeper discussion in Understanding isolatedModules for Transpilation Safety.
8) Handling module interop and import rules
When you have mixed ESModule/CommonJS code, configure esModuleInterop and allowSyntheticDefaultImports in tsconfig to avoid import-related lint errors. Align ESLint import/resolver settings accordingly:
settings: {
'import/resolver': {
typescript: {}
}
}Detailed background on configuring these TypeScript settings is in Configuring esModuleInterop and allowSyntheticDefaultImports: A Practical Guide for Intermediate TypeScript Developers. Proper alignment reduces noisy import-related lint issues.
9) Autofix strategies and Prettier integration
Autofix can be a big productivity boost but also risky. Use autofix for deterministic rules (formatting, semicolons, no-trailing-spaces). For TypeScript-specific auto-fixes, run them locally and in CI in a controlled way. Example npm scripts:
{
"lint": "eslint 'src/**/*.{ts,tsx}'",
"lint:fix": "eslint --fix 'src/**/*.{ts,tsx}'"
}If you use Prettier, integrate via eslint-config-prettier to prevent format rules from colliding with ESLint rules. Prettier should handle formatting; ESLint should focus on code quality and type-safety.
10) Performance and scaling in monorepos
Large projects need caching and scoped linting. Use --cache and consider per-package ESLint configs. For monorepos, create a root config with shared rules and package-level overrides. If you're structuring a large TS repo, anchor your ESLint strategy to project layout guidance from Best Practices for Structuring Large TypeScript Projects to avoid duplication and slow runs. Example fast lint workflow:
eslint --cache --cache-location .eslintcache 'packages/*/src/**/*.{ts,tsx}'Also disable type-aware rules for quick, staged lint runs and run full type-aware lint in CI to catch API-level problems.
Advanced Techniques
Type-aware linting opens possibilities for sophisticated checks: writing custom rules that use TypeScript's type checker to enforce domain-specific invariants (for example, ensuring certain types are not leaked in public APIs). Use @typescript-eslint RuleCreator and TSESTree utilities to implement custom rules. For libraries, enforce that generated declaration files match source types; this is helpful when you combine linting with build-time checks—see Writing declaration files for complex JavaScript libraries and automated generation Generating declaration files automatically (declaration, declarationMap).
You can also configure ESLint to run on declaration files (.d.ts) to ensure API-level constraints. Another advanced approach is to use type-aware rules only in CI (with parserOptions.project) while using a faster no-project variant in local dev—this reduces local lint latency while preserving strict checks centrally. When building APIs that interact with Express or React, pair lint rules with typed middleware and hooks patterns—see guides for Typing Express.js middleware: A Practical TypeScript Guide and Typing React Hooks: A Comprehensive Guide for Intermediate Developers for aligning runtime patterns with lint rules.
Best Practices & Common Pitfalls
Do:
- Prefer @typescript-eslint rules and disable conflicting base rules
- Align linter rules with tsconfig options (strictNullChecks, isolatedModules, esModuleInterop)
- Use 'warn' severity during gradual migrations, escalate to 'error' once stable
- Leverage autofix for deterministic rules; require human review for type-affecting fixes
- Run full type-aware lint in CI; use cached or limited lint locally
Don't:
- Enable every rule blindly—customize to your codebase and team workflow
- Use parserOptions.project for every developer run in a large monorepo without optimizations
- Rely solely on eslint — keep tsc checks in CI to catch compile-time-only errors
Common pitfalls:
- False positives from base ESLint rules (e.g., no-unused-vars); fix by enabling TS variants
- Slow lint runs due to parserOptions.project scanning many files—limit projects or use caching
- Conflicts between code formatters and lint rules—use Prettier + eslint-config-prettier to unify.
Real-World Applications
-
Libraries: Enforce explicit module boundary types, prevent
anyin exported APIs, and ensure declaration files align with code via declaration generation—see Generating declaration files automatically (declaration, declarationMap). -
Server-side apps: Use type-aware rules to prevent unsafe request handlers and to ensure middleware signatures are typed correctly; combine with guidance from Typing Express.js Middleware: A Practical TypeScript Guide.
-
Frontend apps: Enforce strict typing for hooks, props, and contexts. Combine ESLint rules with patterns from Typing React Hooks: A Comprehensive Guide for Intermediate Developers and Typing React Context API with TypeScript — A Practical Guide to ensure components and state are robust.
Conclusion & Next Steps
Integrating ESLint with TypeScript is more than a copy-paste config: it's about aligning lint rules with your compiler settings, team conventions, and project layout. Start small—enable a few high-value rules, run them as warnings, and progressively harden. Use caching and scoped lint runs to keep developer feedback fast, and run full type-aware checks in CI.
Next steps: pick three rules to enforce immediately (no-explicit-any, no-unused-vars via @typescript-eslint, and explicit-module-boundary-types), apply them as 'warn', and iterate from there. For broader project-level decisions, consult deeper TypeScript configuration and structuring guides such as Best Practices for Structuring Large TypeScript Projects and tsconfig guidance in Understanding tsconfig.json compiler option categories.
Enhanced FAQ
Q: Do I need parserOptions.project to use @typescript-eslint? A: No—@typescript-eslint/parser works without a project, but type-aware rules (like @typescript-eslint/no-unsafe-assignment or rules that need the type checker) require parserOptions.project pointing to a valid tsconfig.json. If you enable project mode, lint runs are slower because the parser builds a program. For fast dev feedback, use the non-project mode locally and run type-aware lint in CI.
Q: Which rules should I start with in a TypeScript repo? A: Start with a small set of high-impact rules: replace base rules with TypeScript variants (no-unused-vars, no-shadow), enable @typescript-eslint/no-explicit-any (as warn), and add @typescript-eslint/explicit-module-boundary-types (warn). Use consistent-type-definitions to maintain API consistency. As you stabilize, convert warnings to errors.
Q: How do ESLint rules interact with strictNullChecks? A: Some lint rules rely on nullability semantics. For example, @typescript-eslint/strict-boolean-expressions is more meaningful when strictNullChecks is enabled. If your tsconfig doesn't enable strictNullChecks, the rule may be too noisy or less effective. For migration guidance, check Configuring strictNullChecks in TypeScript: A Practical Guide for Intermediate Developers.
Q: Should I run ESLint and tsc both in CI? Aren't they redundant? A: They are complementary. tsc checks type correctness and compilation-level issues. ESLint enforces style and domain-specific patterns and can use type information to find problems tsc doesn't flag (unused public types, inconsistent patterns, or best-practice violations). Run both—use ESLint to enforce code-quality rules and tsc to guarantee compilation.
Q: What about declaration (.d.ts) files and linting? A: If your library emits declarations, ensure your code and declarations align. You can lint source code rather than .d.ts files, but you should have CI checks that verify declarations (or generate them during CI). For guidance on declarations, read Writing declaration files for complex JavaScript libraries and Generating declaration files automatically (declaration, declarationMap).
Q: How do I avoid performance regressions when enabling project-aware rules in a monorepo? A: Use per-package ESLint configs, enable --cache, and run quick lint passes that skip parserOptions.project locally. Run full type-aware lint (with project set) in CI or scheduled runs. The structure suggestions in Best Practices for Structuring Large TypeScript Projects help design a layout that minimizes cross-package scanning overhead.
Q: How should I configure import resolution to avoid import-related lint problems?
A: Use the import/resolver with the typescript option in ESLint settings and ensure tsconfig paths and module options (esModuleInterop, allowSyntheticDefaultImports) match your runtime bundler settings. For background on the TypeScript module flags, see Configuring esModuleInterop and allowSyntheticDefaultImports: A Practical Guide for Intermediate TypeScript Developers.
Q: Can ESLint enforce runtime-safe patterns for Express or React code? A: Yes—combine ESLint TypeScript rules with type-aware checks to prevent runtime mismatches. For Express, ensure middleware types are correct and handler signatures are explicit; see Typing Express.js middleware: A Practical TypeScript Guide. For React, use rules to enforce typed hooks and context usage—see Typing React Hooks: A Comprehensive Guide for Intermediate Developers and Typing React Context API with TypeScript — A Practical Guide.
Q: Are there rules that are dangerous to autofix?
A: Yes. Autofixes that change types or public signatures can break behavior (for example, explicit-module-boundary-types autofixes might incorrectly infer types). Reserve autofix for formatting and trivial syntactic cleanup. Always run unit tests and type checks after mass autofix runs.
Q: I have third-party libraries without types—what should I do? A: Prefer to add declaration files or use community @types when available. For custom fixes, consider creating manual declaration files and linting your code to avoid using any in public APIs. For help creating declaration files, see Typing Third-Party Libraries Without @types (Manual Declaration Files) and Writing declaration files for complex JavaScript libraries.
Q: How often should I revisit the rule set? A: Re-evaluate the rules at major milestones: before releases, after major refactors, and when onboarding many new contributors. Start with a small consistent set and evolve rules based on real-world pain points from code reviews and bug patterns.
