typescript-eslint
typescript-eslint is the tooling that makes ESLint understand TypeScript. The umbrella package bundles the parser (which turns TS into an AST ESLint can read), the plugin with over 100 TypeScript-specific rules, and helper functions for flat config. Its signature feature is type-aware linting: rules like no-floating-promises and no-unsafe-assignment ask the actual TypeScript type checker questions, catching bug classes that neither plain ESLint nor tsc reports. If ESLint runs on a .ts file anywhere, this project is in the middle of it, and since v8 the tseslint.config helper is the standard way to wire it up.
Mandatory infrastructure for linting TypeScript with ESLint, and the type-aware rules alone justify the setup cost on production codebases. On big repos, pair it with a fast Rust linter for quick feedback and accept that the type-checked run lives in CI.
Use it if
- You run ESLint on any TypeScript codebase; there is no serious alternative for making core ESLint parse TS at all
- You want type-aware rules like no-floating-promises, await-thenable, and no-misused-promises that catch async bugs tsc happily compiles
- You need to layer style and correctness presets (recommended, strict, stylistic, and their type-checked variants) and adjust individual rules per project
- You write custom lint rules for your team and need the typed AST utilities that @typescript-eslint/utils provides
- Lint speed is your top complaint and you do not need type-aware rules: oxlint and Biome lint large repos in a fraction of the time, and type-checked typescript-eslint runs can take minutes on big monorepos
- You only want formatting and basic correctness checks on a small project: Biome gives you both in one Rust binary with near-zero config
- You cannot afford the config tax: type-aware linting means projectService or project wiring, files-versus-tsconfig mismatch errors, and per-package tsconfig juggling in monorepos
- Your TypeScript version runs ahead of support: the peer range caps below TypeScript 6.1, and brand-new compiler releases usually lint with warnings before official support lands
Setup reality
npm install with eslint and typescript as peers, then flat config with tseslint.config gets basic linting in five lines. The pain starts when you enable type-checked presets: you must set parserOptions.projectService (or project) plus tsconfigRootDir, and any file ESLint sees that is not covered by a tsconfig throws a parsing error, which bites config files, scripts, and generated code until you add allowDefaultProject or a disableTypeChecked block. Monorepos multiply this per package. Expect type-aware runs to be several times slower than plain linting since the TypeScript checker does real work, and remember eslint 8.57+, 9, or 10 plus typescript below 6.1 are the supported peers.
Patterns
Minimal flat config for TypeScriptflat-config-quickstart
// eslint.config.mjs
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommended,
);
// run: npx eslint .tseslint.config() gives typed autocomplete and flattens nested arrays; the recommended preset does not need type information.
Turn on type-aware lintingenable-type-checking
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
);projectService finds the right tsconfig per file automatically; without tsconfigRootDir, editors and CLI can resolve projects differently.
Keep type-checked rules off JS filesexclude-js-from-type-check
export default tseslint.config(
tseslint.configs.recommendedTypeChecked,
{
files: ['**/*.js', '**/*.mjs'],
extends: [tseslint.configs.disableTypeChecked],
},
);Without this block, plain JS config files hit "was not found by the project service" parsing errors the first time you lint.
Lint scripts not covered by tsconfiglint-files-outside-tsconfig
export default tseslint.config(
tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: {
allowDefaultProject: ['*.config.ts', 'scripts/*.ts'],
},
tsconfigRootDir: import.meta.dirname,
},
},
},
);allowDefaultProject takes glob patterns relative to the root; keep the list short because default-project files lint noticeably slower.
Adjust a single rule's optionscustomize-rule
export default tseslint.config(
tseslint.configs.recommended,
{
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
);The preset already turns this rule on; your object replaces its options entirely rather than merging with the defaults.
Opt into the strict and stylistic presetsstrict-and-stylistic
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
);strictTypeChecked is a superset of recommendedTypeChecked and the team allows it to change more between minors; pin versions if that bothers you.
Catch unawaited promisescatch-floating-promises
// rule: @typescript-eslint/no-floating-promises (type-checked presets)
async function save() { /* ... */ }
save(); // lint error: floating promise
void save(); // explicit fire-and-forget passes
await save(); // passesThis is the single highest-value type-aware rule; it needs projectService configured or it errors out asking for type information.
Ban specific types with custom messagesban-with-message
rules: {
'@typescript-eslint/no-restricted-types': [
'error',
{
types: {
Object: { message: 'Use object or a specific shape instead.' },
String: { message: 'Use string (lowercase) instead.', fixWith: 'string' },
},
},
],
}v8 split the old ban-types rule; no-restricted-types handles your custom bans while no-wrapper-object-types covers the classic built-in offenders.
Enforce type-only importsenforce-consistent-imports
rules: {
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
],
}Auto-fix rewrites imports to import type or inline type modifiers, which keeps bundlers and isolatedModules happy about erasable imports.
Disable a rule for one line with a reasondisable-rule-inline
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- third-party API returns untyped JSON
const payload: any = await legacyClient.fetch();The -- description is enforceable via the eslint-comments plugin; naked disables are how any leaks back into a strict codebase.
Enforce naming conventionsnaming-convention
rules: {
'@typescript-eslint/naming-convention': [
'error',
{ selector: 'interface', format: ['PascalCase'] },
{ selector: 'typeParameter', format: ['PascalCase'], prefix: ['T'] },
{ selector: 'variable', modifiers: ['const'], format: ['camelCase', 'UPPER_CASE'] },
],
}Selectors are evaluated in order and the first match wins, so put specific selectors before general ones or they never apply.
Scope linting to source directorieslimit-linted-files
export default tseslint.config(
{
ignores: ['dist/**', 'coverage/**', '**/*.generated.ts'],
},
tseslint.configs.recommendedTypeChecked,
// ...
);A config object containing only ignores acts as global ignores in flat config; linting build output is the classic cause of mysterious slowness.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @biomejs/biome | npm | You want linting plus formatting in one fast Rust tool and can live without type-aware rules and the ESLint plugin ecosystem |
| oxlint | npm | You want a very fast first-pass linter, including as a preflight before a slower typescript-eslint run in CI |
| eslint | npm | Your codebase is plain JavaScript, where core ESLint alone covers you without any of this |