Well, that Tuesday debugging session was a real headache, wasn’t it? The culprit? A single, unawaited promise in a background job that silently failed, causing a cascading memory leak that took down our staging cluster. Embarrassing, to say the least.
The worst part wasn’t the downtime. It was realizing that a simple ESLint rule could have caught it three weeks ago when I wrote the code. We tend to treat linters like glorified spellcheckers — tools to nag us about semicolons or indentation — when they should be our safety net against stupidity.
And if you’re writing TypeScript in 2026 without type-aware linting enabled, you’re basically driving a Ferrari with the handbrake on. You have the engine, but you aren’t using the power. Here’s how I actually set up TypeScript ESLint to catch the bugs that AI assistants and tired developers (myself included) keep introducing.
The “Flat Config” Reality Check
Remember the collective groan when ESLint forced the migration to eslint.config.js? Yeah, I hated it too. But now that I’ve converted my fourth monorepo, I have to admit: it’s better. Not easier, just better.
The biggest mistake I see? People copy-pasting the “recommended” config and calling it a day. The recommended config is weak. It’s designed not to annoy beginners. But you aren’t a beginner.
Here’s the setup I’m currently running on a Node 24.2 backend service. It uses typescript-eslint‘s strict type-checked rules.

// eslint.config.mjs
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true, // The game changer for performance
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// My non-negotiables
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
}
);
Catching the “AI Hallucination” Bugs
We all use AI to scaffold code now. It’s faster. But AI is confident and wrong. It loves to invent APIs or assume types that don’t match reality. This is where type-aware linting saves your bacon.
1. The Async Trap
Here’s a classic scenario. You ask an AI to write a function that updates a user profile and logs an event. It gives you this:
async function updateUser(userId: string, data: UserData) {
const user = await db.users.update(userId, data);
// ❌ BAD: This promise is floating!
// If analytics fails, nobody knows. If it throws, unhandled rejection.
analytics.track('user_update', { userId });
return user;
}
The fix isn’t always to await it (you might not want to block). The fix is to be explicit:
async function updateUser(userId: string, data: UserData) {
const user = await db.users.update(userId, data);
// ✅ GOOD: Explicitly handle the background task
void analytics.track('user_update', { userId }).catch((err) => {
logger.error('Analytics failed', err);
});
return user;
}
DOM Manipulation: Where Null Goes to Die
Don’t do this:

function setupModal() {
// ❌ The '!' tells TypeScript "trust me", but runtime doesn't care about trust
const modal = document.getElementById('user-modal')!;
const input = modal.querySelector('input')!;
input.value = 'default';
}
The linter forces you to write defensive code that actually survives in the wild:
function setupModal() {
const modal = document.getElementById('user-modal');
if (!modal) return; // Fail gracefully
const input = modal.querySelector('input');
// Optional chaining is your friend
if (input) {
input.value = 'default';
}
}
API Responses: Trust Nothing
Another area where I see people get lazy is typing API responses. It’s tempting to slap any on a fetch result just to get the red squigglies to go away. But any leaks. It spreads through your codebase like a virus, disabling type checking everywhere it touches.
interface ApiResponse {
id: number;
title: string;
}
async function fetchData(url: string): Promise<ApiResponse> {
const response = await fetch(url);
// ✅ Better: Type assertion (still risky, but explicit)
const data = (await response.json()) as unknown;
if (!isValidResponse(data)) {
throw new Error('Invalid schema');
}
return data;
}
// Simple type guard
function isValidResponse(data: unknown): data is ApiResponse {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'title' in data
);
}
Performance: The Elephant in the Room

Here’s the trick I used to survive the revolt:
- Split the config. We run standard (fast) linting on
git commithooks. It catches syntax errors and style issues instantly. - Run heavy linting in CI only. The type-aware rules run in the pipeline. If you break a type rule, you find out 5 minutes later, not 5 seconds later. It’s a trade-off, but it keeps the local dev loop fast.
- Use
tsc --noEmitfirst. Often, running the TypeScript compiler directly is faster than running ESLint with type information. I usually runtscfirst to catch actual type errors, then let ESLint catch the logic bugs.
Final Thoughts
Tools like TypeScript ESLint aren’t there to make your code “pretty.” They’re there to compensate for the fact that JavaScript is inherently chaotic and our brains (and our AI assistants) are prone to shortcuts. I was skeptical about the strict configs initially. But after avoiding three potential production fires in Q4 2025 solely because the linter flagged a “misused promise” or an “unsafe return,” I’m converted. Turn on the strict rules. Fix the errors. Your future self, debugging at 2 AM on a Tuesday, will thank you.
If you’re looking to improve your TypeScript testing strategy, check out our article on TypeScript Tests That Actually Catch Bugs (And Don’t Make You Cry).
FAQ
What’s the difference between the recommended TypeScript ESLint config and strict type-checked rules?
The recommended config is intentionally weak, designed not to annoy beginners with aggressive warnings. Strict type-checked rules, enabled via tseslint.configs.strictTypeChecked and stylisticTypeChecked, catch deeper bugs by analyzing type information. For a Node 24.2 backend, combining these with projectService: true unlocks type-aware linting that flags floating promises, unsafe returns, and misused thenables that the recommended preset silently ignores.
How do I fix a floating promise warning from @typescript-eslint/no-floating-promises?
You don’t always have to await the promise. If you want the task to run in the background, prefix it with the void operator and chain a .catch handler to log failures, like void analytics.track(‘user_update’, { userId }).catch(err => logger.error(‘Analytics failed’, err)). This makes the fire-and-forget intent explicit while ensuring rejections are still handled instead of crashing as unhandled rejections.
Why is type-aware ESLint linting slow and how do I speed it up in CI?
Type-aware rules require ESLint to load TypeScript’s type information, which is expensive. Split your config: run standard fast linting on git commit hooks to catch syntax and style issues instantly, then run the heavy type-aware rules only in CI. Also run tsc –noEmit first, since the TypeScript compiler is often faster than ESLint with type info for catching pure type errors.
How should I type a fetch response in TypeScript without using any?
Avoid any because it leaks and disables type checking across your codebase. Instead, cast the parsed JSON as unknown, then validate it with a type guard before returning. Write a predicate like isValidResponse(data): data is ApiResponse that checks typeof data === ‘object’, non-null, and required keys like ‘id’ and ‘title’. Throw on invalid schemas so bad responses fail fast rather than propagating.
