Skip to Content
Add typescript-eslint to an Existing TS Repo
Advanced
0 likes
Tutorials

Add typescript-eslint to an Existing TS Repo

Install typescript-eslint on TypeScript 7, switch the rules on as warnings, count them by rule and hold the count down. After this, you can adopt type-aware linting without blocking the team for a week.

Skip ahead: If you already know the TypeScript 7 alias setup and only want the adoption plan, skip to "Step 4: turn the rules on as warnings", then continue.

You have a TypeScript repository that compiles and has no linter, and you want typescript-eslint with type information without a hundred new errors landing in one pull request. This page does it in order: install it in a way that works with TypeScript 7, switch the rules on as warnings, count them, stop the count from growing, and promote the rules one at a time. It takes about twenty minutes and needs network access. The course lesson explains which rules are worth the effort.

Step 1: install with the TypeScript 6 alias

typescript-eslint reads your code through TypeScript's JavaScript API, and TypeScript 7 does not ship one. The TypeScript team publishes @typescript/typescript6 for this, installed under the name typescript, with the real compiler beside it as @typescript/native.

{
  "name": "lintrepo",
  "private": true,
  "type": "module",
  "devDependencies": {
    "@typescript/native": "npm:typescript@7.0.2",
    "typescript": "npm:@typescript/typescript6@^6.0.2",
    "eslint": "10.12.0",
    "@eslint/js": "^10.0.1",
    "typescript-eslint": "8.71.2"
  }
}
npm install --no-audit --no-fund --loglevel=error
npx tsc --version
Version 7.0.2

Your build keeps using tsc 7. Only the linter uses the version 6 API.

Step 2: the repository

This is a small cart module with the kind of code a real repository has.

{
  "compilerOptions": {
    "strict": true,
    "module": "nodenext",
    "target": "es2022",
    "noEmit": true,
    "types": []
  },
  "include": ["src"]
}
export interface Item {
  sku: string;
  qty: number;
}

export async function save(items: Item[]): Promise<void> {
  await new Promise((resolve) => setTimeout(resolve, 5));
  console.log("saved", items.length);
}

export function total(items: Item[]): number {
  return items.reduce((sum, item) => sum + item.qty, 0);
}

export function checkout(items: Item[]): number {
  save(items);
  return total(items);
}
export function parseItem(text: string) {
  const raw = JSON.parse(text);
  return { sku: raw.sku as string, qty: Number(raw.qty) };
}

export function handler(items: string[]) {
  items.forEach(async (text) => {
    console.log(parseItem(text));
  });
}
npx tsc --noEmit
echo "tsc exit code $?"
tsc exit code 0

The compiler is satisfied. That is the starting point for most repositories.

Step 3: the config

ESLint 10 reads eslint.config.mjs. The preset recommendedTypeChecked is the set of rules that use type information, and projectService makes the plugin find the right tsconfig for each file.

Step 4: turn the rules on as warnings

A preset full of errors blocks everyone on day one. The helper below copies the preset and turns every enabled rule into a warning.

import eslint from "@eslint/js";
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";

function asWarnings(configs) {
  return configs.map((config) => ({
    ...config,
    rules: Object.fromEntries(
      Object.entries(config.rules ?? {}).map(([name, setting]) => {
        const level = Array.isArray(setting) ? setting[0] : setting;
        if (level === "off" || level === 0) return [name, setting];
        return [name, Array.isArray(setting) ? ["warn", ...setting.slice(1)] : "warn"];
      }),
    ),
  }));
}

export default defineConfig(
  eslint.configs.recommended,
  asWarnings(tseslint.configs.recommendedTypeChecked),
  {
    languageOptions: {
      parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
    },
  },
);
npx eslint src
echo "eslint exit code $?"

/path/to/project/src/api.ts
  2:9   warning  Unsafe assignment of an `any` value                                     @typescript-eslint/no-unsafe-assignment
  3:21  warning  Unsafe member access .sku on an `any` value                             @typescript-eslint/no-unsafe-member-access
  3:52  warning  Unsafe member access .qty on an `any` value                             @typescript-eslint/no-unsafe-member-access
  7:17  warning  Promise returned in function argument where a void return was expected  @typescript-eslint/no-misused-promises
  7:30  warning  Async arrow function has no 'await' expression                          @typescript-eslint/require-await

/path/to/project/src/cart.ts
  16:3  warning  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

✖ 6 problems (0 errors, 6 warnings)

eslint exit code 0

Six warnings and exit code 0. Nothing fails, and you can see what is there.

Step 5: count by rule

A list of line numbers does not tell you where to start. A count by rule does. ESLint can write its results as JSON to a file, and a short script can total them.

import { readFileSync } from "node:fs";
import { argv, stdout } from "node:process";

const results = JSON.parse(readFileSync(argv[2], "utf8"));
const counts = new Map();

for (const file of results) {
  for (const message of file.messages) {
    const rule = message.ruleId ?? "(parse error)";
    counts.set(rule, (counts.get(rule) ?? 0) + 1);
  }
}

const rows = [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
for (const [rule, count] of rows) {
  stdout.write(`${String(count).padStart(3)} ${rule}\n`);
}
npx eslint src -f json -o report.json
node count.mjs report.json
  2 @typescript-eslint/no-unsafe-member-access
  1 @typescript-eslint/no-floating-promises
  1 @typescript-eslint/no-misused-promises
  1 @typescript-eslint/no-unsafe-assignment
  1 @typescript-eslint/require-await

On a real repository this table is the plan. Pick the rule with the fewest cases and the clearest fix, and clear it first.

Step 6: stop the count from growing

Add a ceiling equal to today's count. --max-warnings makes ESLint exit with an error when there are more warnings than that.

{
  "name": "lintrepo",
  "private": true,
  "type": "module",
  "scripts": {
    "lint": "eslint src --max-warnings 6"
  },
  "devDependencies": {
    "@typescript/native": "npm:typescript@7.0.2",
    "typescript": "npm:@typescript/typescript6@^6.0.2",
    "eslint": "10.12.0",
    "@eslint/js": "^10.0.1",
    "typescript-eslint": "8.71.2"
  }
}
npm run lint --silent
echo "exit code $?"

Now someone adds a new floating promise.

import { save } from "./cart.js";

save([]);
npm run lint --silent 2>&1
echo "exit code $?"

/path/to/project/src/api.ts
  2:9   warning  Unsafe assignment of an `any` value                                     @typescript-eslint/no-unsafe-assignment
  3:21  warning  Unsafe member access .sku on an `any` value                             @typescript-eslint/no-unsafe-member-access
  3:52  warning  Unsafe member access .qty on an `any` value                             @typescript-eslint/no-unsafe-member-access
  7:17  warning  Promise returned in function argument where a void return was expected  @typescript-eslint/no-misused-promises
  7:30  warning  Async arrow function has no 'await' expression                          @typescript-eslint/require-await

/path/to/project/src/cart.ts
  16:3  warning  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

/path/to/project/src/later.ts
  3:1  warning  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

✖ 7 problems (0 errors, 7 warnings)

ESLint found too many warnings (maximum: 6).
exit code 1

The old warnings stay, and the new one breaks the build. Delete the new file. Put npm run lint in CI and the number can only go down.

rm src/later.ts

Step 7: fix one rule and promote it

The call to save(items) in checkout is deliberate: the order is already placed, and saving is a side effect. Say so with void.

export interface Item {
  sku: string;
  qty: number;
}

export async function save(items: Item[]): Promise<void> {
  await new Promise((resolve) => setTimeout(resolve, 5));
  console.log("saved", items.length);
}

export function total(items: Item[]): number {
  return items.reduce((sum, item) => sum + item.qty, 0);
}

export function checkout(items: Item[]): number {
  void save(items);
  return total(items);
}

Now turn that rule into an error for the whole repository, so the problem cannot come back. Lower the ceiling to 5 in package.json, since one warning is gone.

import eslint from "@eslint/js";
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";

function asWarnings(configs) {
  return configs.map((config) => ({
    ...config,
    rules: Object.fromEntries(
      Object.entries(config.rules ?? {}).map(([name, setting]) => {
        const level = Array.isArray(setting) ? setting[0] : setting;
        if (level === "off" || level === 0) return [name, setting];
        return [name, Array.isArray(setting) ? ["warn", ...setting.slice(1)] : "warn"];
      }),
    ),
  }));
}

export default defineConfig(
  eslint.configs.recommended,
  asWarnings(tseslint.configs.recommendedTypeChecked),
  {
    languageOptions: {
      parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
    },
  },
  {
    rules: { "@typescript-eslint/no-floating-promises": "error" },
  },
);
sed -i 's/--max-warnings 6/--max-warnings 5/' package.json
npm run lint --silent
echo "exit code $?"

The rule is now an error, and it has zero cases. Repeat for the next rule: fix, promote, lower the ceiling.

Step 8: lint the config files themselves

If you run eslint . instead of eslint src, ESLint also reads JavaScript files such as the config. Those are not in your tsconfig, so type-aware rules cannot work on them.

npx eslint eslint.config.mjs count.mjs

/path/to/project/count.mjs
  0:0  error  Parsing error: /path/to/project/count.mjs was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject

/path/to/project/eslint.config.mjs
  0:0  error  Parsing error: /path/to/project/eslint.config.mjs was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProject

✖ 2 problems (2 errors, 0 warnings)

Turn the type-checked rules off for plain JavaScript with the preset made for this. Add one block to the config.

import eslint from "@eslint/js";
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";

function asWarnings(configs) {
  return configs.map((config) => ({
    ...config,
    rules: Object.fromEntries(
      Object.entries(config.rules ?? {}).map(([name, setting]) => {
        const level = Array.isArray(setting) ? setting[0] : setting;
        if (level === "off" || level === 0) return [name, setting];
        return [name, Array.isArray(setting) ? ["warn", ...setting.slice(1)] : "warn"];
      }),
    ),
  }));
}

export default defineConfig(
  eslint.configs.recommended,
  asWarnings(tseslint.configs.recommendedTypeChecked),
  {
    languageOptions: {
      parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
    },
  },
  {
    rules: { "@typescript-eslint/no-floating-promises": "error" },
  },
  {
    files: ["**/*.mjs"],
    extends: [tseslint.configs.disableTypeChecked],
  },
);
npx eslint eslint.config.mjs count.mjs
echo "eslint exit code $?"
eslint exit code 0

⚠ Warning: Do not run eslint --fix on the whole repository to clear the warnings. Several of these rules have fixes that change behaviour, for example adding void to a promise nobody checks. Fix one rule at a time and read the diff.

Common mistakes

  • Installing plain typescript@7.0.2 next to typescript-eslint. npm stops with a peer dependency error. Use the alias from Step 1.
  • Forgetting tsconfigRootDir. In a repository with several tsconfig files the plugin may pick the wrong one.
  • Raising the ceiling instead of fixing. If the number goes up in a pull request, ask why.

Related course: TypeScript Advanced.

About

Install typescript-eslint on TypeScript 7, switch the rules on as warnings, count them by rule and hold the count down. After this, you can adopt type-aware linting without blocking the team for a week.

Responsible
Jay P
Last Update
10/08/2026
Reading Time
9 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.