A good ESLint and TypeScript setup does more than catch style issues. It protects type safety, keeps teams consistent, and prevents slow, noisy linting from becoming background friction. This guide gives you a reusable checklist for setting up TypeScript linting with ESLint flat config, choosing rules that add signal instead of churn, and improving performance as your codebase grows. The goal is not a perfect universal config, but a practical baseline you can revisit whenever your framework, build tooling, or team workflow changes.
Overview
If you are building with TypeScript, your linting setup should answer three questions clearly: what code patterns are allowed, which problems belong to the type checker versus the linter, and how much analysis your project can afford on each run. That is the core of a maintainable typescript linting guide.
ESLint works best when it is treated as one layer of feedback, not the only layer. TypeScript handles type correctness. ESLint handles code quality, unsafe patterns, consistency, and framework-specific mistakes. When those responsibilities overlap too much, teams often end up with duplicate warnings, confusing failures, and slow local feedback.
The modern baseline for many projects is ESLint flat config with TypeScript support through the TypeScript ESLint parser and plugin. Flat config simplifies config composition and makes it easier to see what applies to which files. It also encourages more explicit setup, which is helpful in mixed codebases that include TypeScript, JavaScript, tests, scripts, generated files, and framework conventions.
As a working principle, start with these defaults:
- Keep formatting separate from linting unless you have a strong reason not to.
- Use a small set of high-value rules first.
- Enable type-aware lint rules only where they pay for themselves.
- Ignore build output, generated files, and vendored code aggressively.
- Revisit the config when your project structure changes.
If you are still refining your overall TypeScript project layout, it helps to align linting with your repository boundaries and tsconfig structure. See TypeScript Monorepo Guide: Project References, Path Aliases, and Package Boundaries and Node.js with TypeScript: Project Structure, ESM vs CJS, and Build Setup for the repository and compiler side of that decision.
A practical flat config baseline
The exact package versions will change over time, but the structure below captures a stable pattern for eslint typescript setup:
// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default [
{
ignores: [
'dist/**',
'build/**',
'.next/**',
'coverage/**',
'node_modules/**',
'**/*.generated.*'
]
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
projectService: true
}
},
rules: {
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/consistent-type-imports': 'warn',
'@typescript-eslint/no-explicit-any': 'off'
}
}
];This is intentionally restrained. It gives you a modern typescript eslint flat config shape without locking you into a long list of brittle rules on day one.
Checklist by scenario
Use this section as the return-to-it checklist. Pick the scenario closest to your project, then add rules and performance options only when they solve a visible problem.
Scenario 1: New TypeScript project
If you are starting fresh, your priority is clarity. A new project should have a linting setup that every developer can understand in one file.
- Use flat config from the start. Avoid building new projects around legacy config formats unless a framework toolchain still requires it.
- Separate formatting decisions. Let a formatter handle whitespace and punctuation. Reserve ESLint for correctness and maintainability.
- Start with recommended JavaScript and TypeScript rules. This gives a reasonable baseline before you begin project-specific tuning.
- Add a short ignore list early. Exclude build folders, generated code, coverage output, and framework artifacts.
- Decide whether type-aware rules are needed everywhere. For small projects, type-aware linting may be acceptable globally. For larger projects, scope it carefully.
- Set expectations for warnings versus errors. Errors should block merges only when they represent meaningful risk. Warnings are useful for gradual cleanup.
A good starter rule set usually includes:
@typescript-eslint/no-unused-vars@typescript-eslint/consistent-type-imports@typescript-eslint/no-misused-promisesif your project uses async code heavily@typescript-eslint/no-floating-promisesif unhandled async work is a recurring issue@typescript-eslint/ban-ts-commentwith a measured policy, not a blanket ban if migration is in progress
Be careful with strict rules like no-explicit-any. They can be useful in mature codebases, but during early development or migration they may create more noise than value.
Scenario 2: JavaScript to TypeScript migration
Migration projects need a different mindset. The best eslint typescript setup for a migration is one that supports progress without overwhelming the team.
- Lint JavaScript and TypeScript separately when needed. Mixed repositories often need file-pattern-specific config blocks.
- Do not enable every strict TypeScript ESLint rule immediately. First get the repository to a stable passing state.
- Treat unsafe areas as visible debt, not hidden failure. Use warnings, TODO conventions, or scoped overrides for legacy directories.
- Allow pragmatic escape hatches. A temporary
anyor@ts-expect-erroris often better than stalled migration work. - Use lint rules to prevent new debt. It is often more effective to be lenient on old code and strict on new or touched files.
For migration-heavy teams, it helps to pair linting with compiler guidance and targeted error cleanup. The article TypeScript Error Guide: Common Compiler Errors and How to Fix Them is a useful companion when lint findings and type errors start to overlap.
Scenario 3: React and Next.js applications
Frontend apps often carry the most linting layers: TypeScript rules, React rules, hooks rules, accessibility conventions, and framework-specific patterns. The checklist here is to keep those layers from competing.
- Use separate config blocks for app code, tests, and config files.
- Make sure TSX files are included explicitly.
- Enable React Hooks rules if your toolchain does not already provide them.
- Be cautious with rules that fight framework conventions. Some patterns are intentional in Next.js or server/client split architectures.
- Ignore generated route types, build output, and codegen files.
If your linting decisions affect component APIs and props discipline, see React with TypeScript: Best Practices for Props, Hooks, and Component APIs. If your project is Next.js-specific, pair lint setup with architectural choices from Next.js with TypeScript: App Router Patterns, Server Actions, and Type Safety.
Scenario 4: Node.js services and APIs
Backend TypeScript projects usually care less about UI conventions and more about unsafe I/O, async behavior, and boundary typing.
- Prioritize promise-safety rules. Unhandled async work is often more dangerous on the server.
- Use import rules that match your runtime model. ESM and CJS choices influence what “correct” imports look like.
- Apply stricter rules at the API boundary. Request parsing, environment variables, database results, and external service responses are where unsafe assumptions spread.
- Combine linting with runtime validation. ESLint cannot prove external input is valid.
This is where linting should support, not replace, schema validation and typed API design. Related reading: How to Type API Responses in TypeScript for REST, GraphQL, and Fetch Clients and Zod vs Yup vs Valibot: Runtime Validation Libraries for TypeScript Compared.
Scenario 5: Monorepos and large codebases
This is where eslint performance typescript becomes a real concern. A config that feels fine in one package can become painful across twenty.
- Scope config by package or file group. Not every rule needs to run everywhere.
- Limit type-aware linting to packages that benefit most.
- Keep ignore patterns tight and intentional.
- Avoid linting transpiled output, generated SDKs, and snapshots.
- Use caching in local and CI workflows.
- Separate fast checks from deep checks. For example, a quick lint pass on changed files and a fuller pass in CI.
If your repository uses project references or path aliases, linting and compiler configuration should agree on package boundaries. See TypeScript Monorepo Guide: Project References, Path Aliases, and Package Boundaries.
What to double-check
Before you call your setup done, verify the details that most often cause confusion. These checks save more time than adding another ten rules.
1. Are you using the linter for the right jobs?
ESLint should catch risky patterns and consistency issues. TypeScript should catch static type problems. Your formatter should handle style. When one tool starts imitating another, maintenance gets harder.
2. Are type-aware rules actually enabled where you expect?
Some TypeScript ESLint rules only work when ESLint has access to project type information. If a rule appears configured but behaves inconsistently, verify its parser settings and file matching. Flat config makes this easier to inspect, but it also makes mis-scoped config more obvious.
3. Are your file globs too broad?
Many slow lint runs come from globs that include generated artifacts, hidden build folders, or tools directories that do not matter. Narrow the files list and expands only when necessary.
4. Are you duplicating framework behavior?
Some frameworks and meta-frameworks include their own lint presets. If you stack another broad preset on top without checking overlap, you may get duplicate diagnostics or contradictory guidance.
5. Are your rules producing useful code review conversations?
The best eslint rules typescript teams keep are the rules reviewers would otherwise repeat by hand: unsafe promises, unused variables, import consistency, misleading async behavior, and careless suppression comments. If a rule creates mechanical edits with little learning value, reconsider it.
6. Are suppressions visible and intentional?
It is reasonable to permit // eslint-disable-next-line or TypeScript suppression comments in controlled cases. What matters is requiring a reason, limiting scope, and revisiting stale suppressions during cleanup.
7. Is the setup documented in the repository?
A short internal note helps more than a complex config comment block. Include which commands to run, what warnings versus errors mean, and when developers should use overrides. This is especially useful for teams onboarding from JavaScript.
8. Does linting reflect your real architecture?
If you have separate app, server, scripts, and tests folders, your config should say so. Different environments often justify different globals, import rules, and exception policies.
Common mistakes
Most TypeScript linting problems come from overreach, not under-configuring. The following mistakes are common because they feel strict and thorough, but they usually reduce signal.
Enabling every strict rule on day one
This is one of the fastest ways to make linting unpopular. Start with a narrow set of high-value rules, then expand only when a rule prevents a recurring class of issue.
Using ESLint to enforce formatting
Formatting noise hides real diagnostics. Keep formatting automated and separate where possible so lint output remains meaningful.
Turning on type-aware linting everywhere without measuring cost
Type-aware rules can be valuable, but they require more context than syntax-only linting. In larger repositories, it is usually better to reserve them for source code that benefits most.
Ignoring generated code too late
Generated files create noisy warnings, slow runs, and edge cases in parser behavior. Exclude them up front.
Forcing no-explicit-any during active migration
This rule is often well-intentioned and poorly timed. During migration, explicit any can mark an unsafe boundary honestly while other areas are being converted. Use it as a later tightening step, not necessarily an opening move.
Confusing lint failures with runtime safety
Lint rules cannot validate untrusted input, ensure API responses match reality, or guarantee schema alignment. Use runtime validation and typed boundaries for that work. See Zod vs Yup vs Valibot: Runtime Validation Libraries for TypeScript Compared for the validation layer.
Applying identical rules to tests, scripts, and production code
Test files often use different patterns, such as looser assertions, setup variables, or mocking utilities. Build scripts and config files may also need different globals or module rules. Flat config is particularly good at expressing those differences cleanly.
Letting the config grow without pruning
Old overrides, unused plugins, and inherited presets accumulate over time. A config that once solved real problems can become difficult to reason about. Prune regularly.
When to revisit
Linting is not set-and-forget infrastructure. It should be reviewed whenever the cost, architecture, or behavior of the project changes. Use this as your maintenance checklist.
- Revisit before seasonal planning cycles. This is a good time to decide whether stricter rules, cleanup work, or faster CI feedback belong in the next round of engineering work.
- Revisit when workflows or tools change. New framework versions, package manager changes, ESM adoption, monorepo expansion, or build-system changes can all affect lint behavior.
- Revisit when linting becomes noticeably slow. Measure which files and rules are expensive, then tighten globs, caching, and type-aware scope.
- Revisit after a migration milestone. Once the codebase is mostly TypeScript, rules that were too noisy before may become useful.
- Revisit when code review comments repeat. Repeated human feedback often points to a rule worth adding.
- Revisit when suppressions accumulate. A growing number of disable comments usually means a rule is misconfigured, too broad, or solving the wrong problem.
A practical next step is to schedule a short lint audit with this order:
- List current plugins, presets, and overrides.
- Remove anything nobody can justify.
- Identify the top three rule categories that catch real bugs.
- Measure whether type-aware linting is worth its cost in each package.
- Document the policy for suppressions, warnings, and errors.
- Set one future review date tied to a planned tooling or architecture checkpoint.
If you treat your config as a living checklist rather than a one-time setup file, it will stay useful. That is the real goal of a durable TypeScript linting guide: not maximum strictness, but a setup that continues to fit the way your team builds software.