TypeScript — ESLint with typescript-eslint
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
| 1 | npm i -D eslint typescript-eslint typescript @eslint/js |
| 2 |
| 1 | import eslint from "@eslint/js"; |
| 2 | import tseslint from "typescript-eslint"; |
| 3 | |
| 4 | export 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
| Preset | Type-aware | When |
|---|---|---|
| recommended | No | Bootstrap |
| recommendedTypeChecked | Yes | Apps default |
| strictTypeChecked | Yes | Libraries |
| stylisticTypeChecked | Yes | Style |
| 1 | export default tseslint.config( |
| 2 | ...tseslint.configs.strictTypeChecked, |
| 3 | ...tseslint.configs.stylisticTypeChecked, |
| 4 | { rules: { "@typescript-eslint/no-explicit-any": "error" } }, |
| 5 | ); |
| 6 |
| 1 | async function save() {} |
| 2 | save(); // bad |
| 3 | void save(); // good |
| 4 |
| 1 | declare const x: any; |
| 2 | const n: number = x; // unsafe-assignment |
| 3 |
| Rule | Catches |
|---|---|
| no-floating-promises | Unhandled promises |
| no-misused-promises | Async in sync slots |
| no-unsafe-* | any contagion |
| restrict-template-expressions | Bad templates |
| consistent-type-imports | Import style |
| prefer-nullish-coalescing | || vs ?? |
| 1 | export default tseslint.config( |
| 2 | { ignores: ["**/*.gen.ts", "vendor/**"] }, |
| 3 | { files: ["**/*.test.ts"], rules: { "@typescript-eslint/no-explicit-any": "off" } }, |
| 4 | ); |
| 5 |
warning
Use projectService or per-package project arrays. Wrong project = silent rule skips.
| 1 | export default tseslint.config({ |
| 2 | files: ["apps/**/*.{ts,tsx}", "packages/**/*.ts"], |
| 3 | languageOptions: { parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname } }, |
| 4 | }); |
| 5 |
| 1 | void fetchUser(); |
| 2 | if (el) el.focus(); |
| 3 | import type { User } from "./user"; |
| 4 | // @ts-expect-error vendor — missing types |
| 5 | import "untyped"; |
| 6 |
best practice
| 1 | npx tsc --noEmit && npx eslint . --max-warnings=0 |
| 2 |
pro tip
Snippet 1 — float
Practice snippet 1. Predict the resulting type, then confirm with hover types in your editor.
| 1 | void load(); |
Snippet 2 — type import
Practice snippet 2. Predict the resulting type, then confirm with hover types in your editor.
| 1 | import type { A } from "./a"; |
Snippet 3 — unknown
Practice snippet 3. Predict the resulting type, then confirm with hover types in your editor.
| 1 | function 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.
| 1 | const n = v ?? 0; |
Snippet 5 — optional
Practice snippet 5. Predict the resulting type, then confirm with hover types in your editor.
| 1 | user?.name |
Snippet 6 — throw
Practice snippet 6. Predict the resulting type, then confirm with hover types in your editor.
| 1 | throw new Error("x"); |
Snippet 7 — template
Practice snippet 7. Predict the resulting type, then confirm with hover types in your editor.
| 1 | `${String(n)}` |
Snippet 8 — disable
Practice snippet 8. Predict the resulting type, then confirm with hover types in your editor.
| 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.
| 1 | onClick={() => { void save(); }} |
Snippet 10 — await
Practice snippet 10. Predict the resulting type, then confirm with hover types in your editor.
| 1 | await Promise.resolve(1); |
note
This deep dive expands eslint with production-oriented recipes. Skim the tables, then implement one recipe in a scratch file under strict mode.
info
Recipe 1: Core pattern. Adapt names to your domain; keep the type relationships intact.
| 1 | export type Flag = "on" | "off"; |
| 2 | export function toggle(f: Flag): Flag { |
| 3 | return f === "on" ? "off" : "on"; |
| 4 | } |
| 5 |
note
Recipe 2: Boundary parse. Adapt names to your domain; keep the type relationships intact.
| 1 | export function parse(input: unknown): string { |
| 2 | if (typeof input !== "string") throw new Error("string"); |
| 3 | return input; |
| 4 | } |
| 5 |
note
Recipe 3: Exhaustive. Adapt names to your domain; keep the type relationships intact.
| 1 | function assertNever(x: never): never { throw new Error(String(x)); } |
| 2 | type K = "a" | "b"; |
| 3 | export 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
Recipe 4: Readonly params. Adapt names to your domain; keep the type relationships intact.
| 1 | export function sum(nums: readonly number[]): number { |
| 2 | return nums.reduce((a, b) => a + b, 0); |
| 3 | } |
| 4 |
note
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.
| Question | Short 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 |
| Item | Status |
|---|---|
| Strict tsconfig enabled | ☐ |
| Boundaries validated at runtime | ☐ |
| Public generics documented for variance | ☐ |
| Lint type-checked rules on | ☐ |
| No casual any / ts-ignore | ☐ |
best practice
You covered eslint end-to-end: core mental model, recipes, FAQ, and a ship checklist. Continue with related topics via PageNav.
pro tip
End-to-end worked example for eslint. Copy into a scratch project with strict: true.
| 1 | export type Flag = "on" | "off"; |
| 2 | export 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
| Step | Pass criteria |
|---|---|
| Compiles under strict | No errors |
| Intentional misuse fails | @ts-expect-error lights up |
| Runtime path tested | Vitest or node assert |
| Docs updated | README / PR note |
| 1 | import type { Expect, Equal } from "./type-tests"; |
| 2 | |
| 3 | // Local helpers if you lack a type-test lib: |
| 4 | type Equal<A, B> = |
| 5 | (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false; |
| 6 | type Expect<T extends true> = T; |
| 7 | |
| 8 | type _smoke = Expect<Equal<true, true>>; |
| 9 | // Topic: eslint |
| 10 |
best practice
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.
| 1 | npx tsc --noEmit |
| 2 | npx eslint . |
| 3 | git diff --exit-code # ensure no accidental emit |
| 4 |
warning
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.
| 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
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.