|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/eslint
$cat docs/typescript-—-eslint-with-typescript-eslint.md
updated Today·20 min read·published

TypeScript — ESLint with typescript-eslint

TypeScriptToolingESLintIntermediate🎯Free Tools
Introduction

typescript-eslint brings type-aware lint rules to ESLint: floating promises, unsafe any, and incorrect async handlers that tsc may allow.

This guide covers flat config, recommendedTypeChecked vs strictTypeChecked, ignore patterns, monorepo wiring, CI, and the fixes you will apply daily.

info

Turn on type-checked lint after strict tsconfig — otherwise you fight the same bugs twice in different orders.
Installation
install.sh
Bash
1npm i -D eslint typescript-eslint typescript @eslint/js
2
eslint.config.mjs
JavaScript
1import eslint from "@eslint/js";
2import tseslint from "typescript-eslint";
3
4export default tseslint.config(
5 { ignores: ["dist/**", "coverage/**", ".next/**", "node_modules/**"] },
6 eslint.configs.recommended,
7 ...tseslint.configs.recommendedTypeChecked,
8 {
9 languageOptions: {
10 parserOptions: {
11 projectService: true,
12 tsconfigRootDir: import.meta.dirname,
13 },
14 },
15 rules: {
16 "@typescript-eslint/no-floating-promises": "error",
17 "@typescript-eslint/no-misused-promises": "error",
18 "@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
19 },
20 },
21);
22
📝

note

Prefer projectService on typescript-eslint v8+.
Presets
PresetType-awareWhen
recommendedNoBootstrap
recommendedTypeCheckedYesApps default
strictTypeCheckedYesLibraries
stylisticTypeCheckedYesStyle
strict.mjs
JavaScript
1export default tseslint.config(
2 ...tseslint.configs.strictTypeChecked,
3 ...tseslint.configs.stylisticTypeChecked,
4 { rules: { "@typescript-eslint/no-explicit-any": "error" } },
5);
6
High-Value Rules
float.ts
TypeScript
1async function save() {}
2save(); // bad
3void save(); // good
4
unsafe.ts
TypeScript
1declare const x: any;
2const n: number = x; // unsafe-assignment
3
RuleCatches
no-floating-promisesUnhandled promises
no-misused-promisesAsync in sync slots
no-unsafe-*any contagion
restrict-template-expressionsBad templates
consistent-type-importsImport style
prefer-nullish-coalescing|| vs ??
Ignores & Overrides
ign.mjs
JavaScript
1export default tseslint.config(
2 { ignores: ["**/*.gen.ts", "vendor/**"] },
3 { files: ["**/*.test.ts"], rules: { "@typescript-eslint/no-explicit-any": "off" } },
4);
5

warning

Put ignores first in flat config.
Monorepo

Use projectService or per-package project arrays. Wrong project = silent rule skips.

mono.mjs
JavaScript
1export default tseslint.config({
2 files: ["apps/**/*.{ts,tsx}", "packages/**/*.ts"],
3 languageOptions: { parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname } },
4});
5
Common Fixes
fixes.ts
TypeScript
1void fetchUser();
2if (el) el.focus();
3import type { User } from "./user";
4// @ts-expect-error vendor — missing types
5import "untyped";
6

best practice

Fix types over disabling rules; when disabling, cite a reason.
CI
ci.sh
Bash
1npx tsc --noEmit && npx eslint . --max-warnings=0
2
🔥

pro tip

Separate tsc and eslint steps for clearer failures.
Practice Snippets

Snippet 1 — float

Practice snippet 1. Predict the resulting type, then confirm with hover types in your editor.

eslint-1.ts
TypeScript
1void load();

Snippet 2 — type import

Practice snippet 2. Predict the resulting type, then confirm with hover types in your editor.

eslint-2.ts
TypeScript
1import type { A } from "./a";

Snippet 3 — unknown

Practice snippet 3. Predict the resulting type, then confirm with hover types in your editor.

eslint-3.ts
TypeScript
1function f(x: unknown) { if (typeof x === "string") return x; throw new Error(); }

Snippet 4 — nullish

Practice snippet 4. Predict the resulting type, then confirm with hover types in your editor.

eslint-4.ts
TypeScript
1const n = v ?? 0;

Snippet 5 — optional

Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.

eslint-5.ts
TypeScript
1user?.name

Snippet 6 — throw

Practice snippet 6. Predict the resulting type, then confirm with hover types in your editor.

eslint-6.ts
TypeScript
1throw new Error("x");

Snippet 7 — template

Practice snippet 7. Predict the resulting type, then confirm with hover types in your editor.

eslint-7.ts
TypeScript
1`${String(n)}`

Snippet 8 — disable

Practice snippet 8. Predict the resulting type, then confirm with hover types in your editor.

eslint-8.ts
TypeScript
1// eslint-disable-next-line @typescript-eslint/no-explicit-any -- stub

Snippet 9 — misused

Practice snippet 9. Predict the resulting type, then confirm with hover types in your editor.

eslint-9.ts
TypeScript
1onClick={() => { void save(); }}

Snippet 10 — await

Practice snippet 10. Predict the resulting type, then confirm with hover types in your editor.

eslint-10.ts
TypeScript
1await Promise.resolve(1);
📝

note

These snippets are intentionally dense. Prefer understanding one fully over skimming all.
Deep Dive — eslint

This deep dive expands eslint with production-oriented recipes. Skim the tables, then implement one recipe in a scratch file under strict mode.

info

Keep npx tsc --noEmit green while experimenting — type errors are the curriculum.
Recipe — Core pattern

Recipe 1: Core pattern. Adapt names to your domain; keep the type relationships intact.

eslint-a.ts
TypeScript
1export type Flag = "on" | "off";
2export function toggle(f: Flag): Flag {
3 return f === "on" ? "off" : "on";
4}
5
📝

note

Verify recipe 1 by hovering inferred types and attempting an intentional misuse.
Recipe — Boundary parse

Recipe 2: Boundary parse. Adapt names to your domain; keep the type relationships intact.

eslint-b.ts
TypeScript
1export function parse(input: unknown): string {
2 if (typeof input !== "string") throw new Error("string");
3 return input;
4}
5
📝

note

Verify recipe 2 by hovering inferred types and attempting an intentional misuse.
Recipe — Exhaustive

Recipe 3: Exhaustive. Adapt names to your domain; keep the type relationships intact.

eslint-c.ts
TypeScript
1function assertNever(x: never): never { throw new Error(String(x)); }
2type K = "a" | "b";
3export function f(k: K) {
4 switch (k) {
5 case "a": return 1;
6 case "b": return 2;
7 default: return assertNever(k);
8 }
9}
10
📝

note

Verify recipe 3 by hovering inferred types and attempting an intentional misuse.
Recipe — Readonly params

Recipe 4: Readonly params. Adapt names to your domain; keep the type relationships intact.

eslint-d.ts
TypeScript
1export function sum(nums: readonly number[]): number {
2 return nums.reduce((a, b) => a + b, 0);
3}
4
📝

note

Verify recipe 4 by hovering inferred types and attempting an intentional misuse.
FAQ

Does this replace runtime checks?

No. TypeScript erase at compile time. Pair with Zod or hand validation at boundaries.

How do I migrate gradually?

Enable strict flags one at a time, fix a package, then expand. See the migration guide.

What about performance?

Prefer project references and narrow include globs. Avoid enormous declaration merges in hot paths of the checker.

QuestionShort answer
Runtime cost of brands/variance annotations?Zero — compile-time only
Need emit?Often noEmit / bundler handles emit
CI gate?tsc --noEmit + ESLint type-checked
Checklist
ItemStatus
Strict tsconfig enabled
Boundaries validated at runtime
Public generics documented for variance
Lint type-checked rules on
No casual any / ts-ignore

best practice

Turn this checklist into a PR template for TypeScript-heavy changes.
Summary

You covered eslint end-to-end: core mental model, recipes, FAQ, and a ship checklist. Continue with related topics via PageNav.

🔥

pro tip

Teach teammates the failure modes first — correct code is easier after you have seen the bugs.
Worked Example

End-to-end worked example for eslint. Copy into a scratch project with strict: true.

eslint-worked.ts
TypeScript
1export type Flag = "on" | "off";
2export function toggle(f: Flag): Flag {
3 return f === "on" ? "off" : "on";
4}
5

Step-by-step

1. Paste the snippet. 2. Introduce a deliberate type error. 3. Fix it without assertions. 4. Add a second consumer that should fail if variance/brands/refs are wrong.

5. Run npx tsc --noEmit. 6. Commit the learning as a unit test or type test with @ts-expect-error.

info

If the example feels large, extract types into a dedicated file and keep runtime logic thin.
StepPass criteria
Compiles under strictNo errors
Intentional misuse fails@ts-expect-error lights up
Runtime path testedVitest or node assert
Docs updatedREADME / PR note
eslint-type-test.ts
TypeScript
1import type { Expect, Equal } from "./type-tests";
2
3// Local helpers if you lack a type-test lib:
4type Equal<A, B> =
5 (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
6type Expect<T extends true> = T;
7
8type _smoke = Expect<Equal<true, true>>;
9// Topic: eslint
10

best practice

Keep type tests in CI alongside unit tests — they are documentation that cannot rot silently.
Production Notes

Shipping notes teams hit in production when adopting these patterns: version skew between editor TypeScript and CI, incomplete include globs, and silent any from third-party DefinitelyTyped stubs.

verify.sh
Bash
1npx tsc --noEmit
2npx eslint .
3git diff --exit-code # ensure no accidental emit
4

warning

Never commit skipLibCheck: false flips without measuring CI time — but do not use skipLibCheck to hide broken local types.

Document peer dependency TypeScript ranges for libraries. For apps, pin the TypeScript version in the workspace and enable the workspace SDK in VS Code/Cursor.

package-engines.json
JSON
1{
2 "devDependencies": {
3 "typescript": "~5.8.0"
4 },
5 "packageManager": "pnpm@9"
6}
7

Rollback plan

If a strict flag floods errors, revert the flag, keep fixed files, and re-enable on a smaller glob via a nested tsconfig. Progress should be monotonic even when flags temporarily retreat.

🔥

pro tip

Track error counts in CI comments so migrations stay visible to the whole team.
$Blueprint — Engineering Documentation·Section ID: TS-ESLINT·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.