12k
All articles

Linting TypeScript with ESLint

ESLint 10 flat config for TypeScript: setup with typescript-eslint, enable typed linting with projectService, and add Prettier cleanly.

OpenReplay Team
OpenReplay Team
Linting TypeScript with ESLint

As of July 2026, the current way to lint TypeScript is ESLint 10 with the typescript-eslint package in flat config (eslint.config.mjs) — not the old .eslintrc setup that most search results still show.

If you’ve ever pasted a config from a 2022 tutorial and watched ESLint ignore it completely, this is why: it was written for a config system that no longer exists. The replacement is short, though the type-aware part needs one extra option that’s easy to miss.

ESLint 10 removed the eslintrc config system outright, as the project had signalled in its flat config rollout plans. That single change breaks nearly every pre-2024 tutorial, because ESLint no longer reads .eslintrc or .eslintignore files at all. This guide gives you a correct, copy-pasteable flat config for TypeScript, shows how to turn on type-aware rules, and wires linting into your scripts, editor, and CI.

Key Takeaways

  • The modern stack is ESLint 10 plus typescript-eslint v8 in flat config; .eslintrc/.eslintignore are dead as of ESLint 10.
  • A minimal config passes js.configs.recommended and tseslint.configs.recommended to defineConfig() from eslint/config, in a file named eslint.config.js/.mjs.
  • Type-aware rules like no-floating-promises need parserOptions: { projectService: true }. An empty parserOptions does not enable them.
  • Typed linting asks TypeScript to build your project before linting, so it’s slower; run it in CI and rely on IDE caching in the editor.
  • In flat config the --ext flag is obsolete: file targeting lives in each block’s files glob, so the lint script is just eslint ..

Do ESLint and TypeScript do the same job?

ESLint and TypeScript are complementary, not competing. A handful of typescript-eslint rules do reach into TypeScript’s type checker for a deeper read on your code, but the two tools answer different questions: TypeScript’s compiler checks that types line up, while ESLint enforces style and catches likely bugs (unused variables, floating promises, unsafe patterns) across your codebase. You run both.

If you’re migrating from TSLint, note that it’s been dead for years. Its backers announced in 2019 that they would deprecate it in favour of typescript-eslint, and the ESLint ecosystem became the standard for linting TypeScript. There is no reason to reach for TSLint in a new project.

One prerequisite before you install: ESLint 10 dropped older Node versions. It now runs on Node.js v20.19.0 and above, v22.13.0 and above, or v24 and above, and v21.x and v23.x are no longer supported.

How do you set up ESLint for TypeScript?

Install the four packages you actually need:

npm i -D eslint @eslint/js typescript typescript-eslint

The typescript-eslint helper bundles the parser and plugin, so you don’t wire @typescript-eslint/parser and @typescript-eslint/eslint-plugin by hand. It supports the current major: typescript-eslint’s documented ESLint range covers ^8.57.0 || ^9.0.0 || ^10.0.0, so typescript-eslint@latest (v8.x) runs cleanly on ESLint 10.

Create eslint.config.mjs (flat config, not .eslintrc):

// eslint.config.mjs
import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';

export default defineConfig(
  js.configs.recommended,
  tseslint.configs.recommended,
);

That’s a working baseline: ESLint’s core recommended rules plus typescript-eslint’s recommended set, which specifies the typescript-eslint parser and plugin for you. defineConfig() comes from ESLint core and is the helper to reach for now, because typescript-eslint has deprecated its own tseslint.config() in favour of it. The old helper still runs, so a config that already works isn’t broken, but new setups should use defineConfig(). Keep importing tseslint either way, since you still need it for tseslint.configs.* and the glob helpers.

Go stricter, then tune individual rules

recommended is the starting point; two opt-in presets raise the bar. tseslint.configs.strict adds more opinionated correctness rules, and tseslint.configs.stylistic adds consistency rules that don’t need type information. Add them alongside recommended in the config array.

Override any rule in a rules block. Severities come in three levels: off (or 0) switches the rule off entirely, warn (or 1) reports the problem without affecting the exit code, and error (or 2) reports it and makes ESLint exit with code 1. Use warn for things you want visible but non-blocking; use error for anything that must not reach the repo, since it exits non-zero and fails CI.

rules: {
  '@typescript-eslint/no-explicit-any': 'warn',
  '@typescript-eslint/no-unused-vars': 'error',
}

Prefer string severities ('warn'/'error') over the numeric form in modern configs. They read more clearly, and the numeric-only style is a hallmark of dated .eslintrc tutorials.

Type-aware linting: the rules that need type information

Some of the most valuable rules, no-floating-promises and no-misused-promises among them, need type information, and you enable it by adding parserOptions: { projectService: true }. That has been the recommended way to switch on typed linting since typescript-eslint v8, replacing the older project option because it takes less configuration and runs faster. Also switch your presets to their type-checked variants (recommendedTypeChecked, strictTypeChecked, stylisticTypeChecked). An empty parserOptions: {} does not turn on type-aware linting, a common mistake in copied configs.

{
  files: ['**/*.ts', '**/*.tsx'],
  extends: [tseslint.configs.recommendedTypeChecked],
  languageOptions: {
    parserOptions: {
      projectService: true,
      tsconfigRootDir: import.meta.dirname,
    },
  },
}

Typed linting has a real cost. Turning it on means TypeScript has to build your project before ESLint can lint it, which is a second or two on a small codebase and noticeably longer on a large one. typescript-eslint’s own advice leans on an asymmetry here: editor plugins cache type information and largely escape the penalty, so run the full typed lint in CI and pre-commit, and let the editor cover you day to day. projectService also removes the old workaround of maintaining a separate tsconfig.eslint.json, since it uses the same project the editor does.

Split JS from TS, and set your ignores

Type-checked rules only make sense on files TypeScript understands, so scope them to **/*.ts/**/*.tsx and turn them off for plain JavaScript. typescript-eslint ships a preset for exactly this. Its own docs apply tseslint.configs.disableTypeChecked to a **/*.js block to strip the TypeScript-specific setup back out. In flat config, ignores are just a config block with only an ignores key, which is what replaces .eslintignore.

// eslint.config.mjs
import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';

export default defineConfig(
  { ignores: ['dist/', 'node_modules/', 'coverage/', '**/*.d.ts'] },
  js.configs.recommended,
  {
    files: ['**/*.ts', '**/*.tsx'],
    extends: [tseslint.configs.recommendedTypeChecked],
    languageOptions: {
      parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
    },
    rules: { '@typescript-eslint/no-explicit-any': 'warn' },
  },
  { files: ['**/*.js', '**/*.mjs'], extends: [tseslint.configs.disableTypeChecked] },
  prettier, // must be last
);

Let Prettier format, then wire the workflow

Keep formatting out of ESLint. Add eslint-config-prettier last to switch off ESLint’s stylistic rules that would fight Prettier, and pin it to ^10.1.8 or later. That version matters: in July 2025, a phishing attack on a maintainer’s npm credentials led to four tampered releases, tracked as CVE-2025-54313. Versions 8.10.1, 9.1.1, 10.1.6 and 10.1.7 shipped a postinstall script that ran a bundled DLL payload on Windows machines, and the fixed releases are 8.10.2, 9.1.2 and 10.1.8. Only those four were affected and the payload only ran on Windows, so clean earlier builds such as 10.1.5 were never compromised. Running Prettier as an ESLint rule via eslint-plugin-prettier is possible but optional; many teams skip it because it makes linting slower and noisier.

Add a lint script. No --ext flag is needed, because file targeting lives in each config block’s files glob:

{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}

From there, run eslint --fix on staged files with Husky and lint-staged before each commit, enable fix-on-save in VS Code via "source.fixAll.eslint": "explicit" in codeActionsOnSave, and run eslint . as a CI step so a failing rule blocks the merge.

One last thing worth acting on: ESLint 9 reached end of life on 2026-08-06 and receives no further updates. If you’re still on ESLint 9, the config above works unchanged on ESLint 10, so upgrade the runtime and move on. Start from the minimal two-line config, add recommendedTypeChecked with projectService when you want the promise-safety rules, and put eslint-config-prettier last.

FAQs

Should I enable type-aware linting, and what does it cost?

Enable it if you want the highest-value correctness rules like no-floating-promises and no-misused-promises, which cannot work without type information. The cost is that ESLint asks TypeScript to build your project before linting, which is negligible on small projects but noticeable on large ones. Most teams run the full typed lint in CI and pre-commit and rely on IDE caching in the editor, where the penalty is avoided.

What is the difference between projectService and project for typed linting?

Both enable typed linting, but projectService is what typescript-eslint recommends as of v8 for easier configuration and faster linting, because it reuses the same tsconfig.json your editor already uses. The older project option requires you to point at one or more TSConfig files by path and often forced teams to maintain a separate tsconfig.eslint.json. Use projectService: true unless you have a specific reason not to.

Does the --ext flag still work in ESLint flat config?

No, --ext is no longer needed in flat config. File targeting lives inside each config block's files glob, for example files: ['**/*.ts', '**/*.tsx'], so ESLint already knows which files to lint. Your lint script becomes just eslint . with no extension flag. Scripts that still pass --ext are copied from pre-flat-config tutorials written for the removed eslintrc system.

Should I use eslint-config-prettier or eslint-plugin-prettier?

Use eslint-config-prettier for most projects. It turns off ESLint's stylistic rules that conflict with Prettier and adds no runtime overhead; place it last in your config array. The eslint-plugin-prettier approach runs Prettier as an actual lint rule, which is optional and slower, and it surfaces every formatting difference as a lint error. Pin eslint-config-prettier to 10.1.8 or later to stay clear of the July 2025 supply-chain incident.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.