Linting with typescript-eslint
The TypeScript compiler answers one question: "do the types fit?" Many bugs pass that test. A promise that is never awaited, any values spreading through the code, a switch that silently misses a new case, an import that should be import type. A linter is a second tool that checks your code against rules for such patterns.
ESLint is the standard JavaScript linter, and typescript-eslint is the project that lets ESLint read TypeScript and adds rules that use type information. Nearly every professional TypeScript codebase runs it in the editor and in CI. In this lesson you will set it up, understand the presets, and read the most valuable rules.
Installing ESLint and typescript-eslint
Install three development packages:
npm install --save-dev eslint @eslint/js typescript-eslintThere is one TypeScript 7 detail. typescript-eslint's type-aware rules call the TypeScript compiler as a library, and TypeScript 7.0 has no such API yet (see TypeScript 7 Compiler & Upgrading). The typescript-eslint 8 release used for this lesson (8.72.1) declares support for TypeScript below 6.1. So in a TypeScript 7 project, let typescript resolve to the TypeScript 6 compatibility package and get the TypeScript 7 compiler under another name:
{
"devDependencies": {
"typescript": "npm:@typescript/typescript6@^6.0.2",
"@typescript/native": "npm:typescript@^7.0.2"
}
}Your tsc command still runs TypeScript 7, while ESLint uses TypeScript 6 behind the scenes. Check the typescript-eslint website for the current status; this step disappears once it supports the new API.
The config file
ESLint uses a flat config file, eslint.config.mjs, at the project root. This one turns on ESLint's own recommended rules plus typescript-eslint's type-aware recommended rules:
// @ts-check
import eslint from "@eslint/js";
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
export default defineConfig(
eslint.configs.recommended,
tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
},
},
},
{
files: ["**/*.mjs"],
extends: [tseslint.configs.disableTypeChecked],
},
);projectService: true tells typescript-eslint to find the right tsconfig.json for each file the same way your editor does. The last block switches type-aware rules off for plain JavaScript config files, which are not part of your TypeScript project. The // @ts-check comment at the top makes your editor check the config file itself.
Run it with npx eslint src, or add "lint": "eslint ." to the scripts in package.json.
What type-aware rules catch
Here is a small checkout function. It compiles without a single TypeScript error:
type Order = { id: string; total: number };
async function saveOrder(order: Order): Promise<void> {
await new Promise((r) => setTimeout(r, 10));
console.log(`saved ${order.id}`);
}
export function checkout(raw: string): void {
const data: any = JSON.parse(raw);
const order: Order = data;
saveOrder(order);
[order].forEach(async (o) => await saveOrder(o));
}ESLint with recommendedTypeChecked finds five problems:
src/orders.ts 9:9 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment 9:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 10:9 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment 11:3 error Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator @typescript-eslint/no-floating-promises 12:19 error Promise returned in function argument where a void return was expected @typescript-eslint/no-misused-promises ✖ 5 problems (5 errors, 0 warnings)
(ESLint prints the full path of each file; it is shortened in this lesson.) Each rule maps to a real production bug:
no-explicit-anyandno-unsafe-assignment:JSON.parsereturnsany, and assigning it toOrderturns off checking without anyone noticing. Parse intounknownand validate instead (see Runtime Validation & Type-Safe APIs).no-floating-promises:saveOrder(order);starts the save but nobody waits for it or handles its failure. If the database is down, the error disappears.no-misused-promises:forEachignores the promise an async callback returns, so the loop does not wait (you saw this in Typing Async Code).
These rules need type information, which is why they are called type-aware. A plain syntax linter cannot know that saveOrder returns a promise.
Choosing presets
typescript-eslint ships ready-made rule sets. Pick one row from each column of this table:
| Preset | Rules |
|---|---|
recommended / recommendedTypeChecked | Rules for clear bugs and bad practice; a safe start for any project |
strict / strictTypeChecked | Everything in recommended plus more opinionated rules that catch subtler bugs |
stylistic / stylisticTypeChecked | Consistent style, such as one way to define object types; no bug-finding |
The ...TypeChecked versions include every rule from the plain version plus the type-aware ones. Teams starting a new project often choose strictTypeChecked and stylisticTypeChecked. For an existing codebase, start with recommendedTypeChecked so the first run does not report thousands of problems.
You can also turn on individual rules in a rules block. Two that are not in the presets but are worth adding:
rules: {
"@typescript-eslint/switch-exhaustiveness-check": "error",
"@typescript-eslint/consistent-type-imports": "error",
},With the strict and stylistic presets plus these two rules, this fare module gets three reports:
// src/types.ts
export type Coach = "SL" | "3A" | "2A" | "CC";
export type Passenger = { name: string; age: number };
// src/fare.ts
import { Coach, Passenger } from "./types.js";
export function baseFare(coach: Coach): number {
switch (coach) {
case "SL":
return 450;
case "3A":
return 1150;
case "2A":
return 1650;
}
return 0;
}src/fare.ts 1:1 error All imports in the declaration are only used as types. Use `import type` @typescript-eslint/consistent-type-imports 4:11 error Switch is not exhaustive. Cases not matched: "CC" @typescript-eslint/switch-exhaustiveness-check src/types.ts 2:13 error Use an `interface` instead of a `type` @typescript-eslint/consistent-type-definitions ✖ 3 problems (3 errors, 0 warnings) 2 errors and 0 warnings potentially fixable with the `--fix` option.
The missing "CC" (AC Chair Car) case is a real bug. The final return 0 keeps TypeScript quiet, so a chair-car ticket would silently cost nothing:
type Coach = "SL" | "3A" | "2A" | "CC";
function baseFare(coach: Coach): number {
switch (coach) {
case "SL":
return 450;
case "3A":
return 1150;
case "2A":
return 1650;
}
return 0;
}
for (const c of ["SL", "CC"] as const) {
console.log(`${c}: ₹${baseFare(c)}`);
}SL: ₹450 CC: ₹0
The consistent-type-definitions report is a style choice: the stylistic preset prefers interface for object types. If your team prefers type, configure the rule as ["error", "type"]. Style rules should match a team decision, not the other way round.
Living with a linter
- Auto-fix.
npx eslint . --fixapplies safe fixes, such as converting toimport type. Review the diff before committing. - Disabling a rule once. Write
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- reasonabove the line. Always name the rule and give a reason after--. - CI. Run
npm run lintin your pipeline next totsc --noEmit, so problems are caught before review. - Speed. Type-aware linting is slower than type checking alone, because it builds type information too. On large repos, lint changed files locally and the whole project in CI.
Common mistakes
- Using the type-aware presets without
projectService. ESLint stops with an error saying the rules need type information. - Silencing rules everywhere with
eslint-disable. Each comment hides a possible bug. Fix the code or change the rule in config. - Treating style rules as bugs. Agree on stylistic rules as a team; keep bug-finding rules strict.
- Linting build output. Add
distand other generated folders to anignoresentry in the config.
Key points
- ESLint with typescript-eslint catches bugs that type checking alone misses, such as floating promises and unsafe
any. - The flat config
eslint.config.mjscombines@eslint/jsrules with typescript-eslint presets;projectService: trueenables type-aware rules. - Choose
recommendedTypeCheckedfor existing code andstrictTypeCheckedplusstylisticTypeCheckedfor new projects. switch-exhaustiveness-checkandconsistent-type-importsare valuable extra rules.- In a TypeScript 7 project, typescript-eslint currently uses TypeScript 6 through the
@typescript/typescript6alias.
Exercise
A Jaipur handicrafts shop's code has these lines: const items: any = await res.json();, items.forEach(async (i) => await updateStock(i)); and sendInvoice(orderId); where updateStock and sendInvoice return promises. For each line, name the typescript-eslint rule that reports it and rewrite the line so the rule passes.
Show solution
// no-explicit-any + no-unsafe-assignment: parse into unknown, then validate
const raw: unknown = await res.json();
const items = parseItems(raw); // your schema or type guard returns Item[]
// no-misused-promises: forEach ignores promises; wait for all updates
await Promise.all(items.map((i) => updateStock(i)));
// no-floating-promises: await it (or handle errors explicitly)
await sendInvoice(orderId);Each fix makes the hidden behaviour visible: any is replaced by checked data, the stock updates are really awaited, and a failed invoice now surfaces as an error instead of disappearing.
Quick Quiz
1. Why does no-floating-promises need type information?
Type-aware rules ask the TypeScript compiler about the types of expressions.
2. What does projectService: true do in the config?
It uses the same project lookup as the editor.
3. Which preset should you start with in a large existing codebase?
Start with fewer, clearly valuable rules, then tighten.
4. Why does a TypeScript 7 project need the @typescript/typescript6 alias for type-aware linting today?
TypeScript 7.1 is expected to bring a new API that tools can adopt.
Learn TypeScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
