34.1 @typescript-eslint: Linting TypeScript with Type-Aware Rules
Right, let’s talk about making your code not just work, but not be an absolute embarrassment to look at. We’ve all been there: you’re three coffees deep, you’ve just hacked together a function that somehow works, and you’d rather delete the repository than have a senior dev look at it. That’s where @typescript-eslint comes in—it’s the brilliant, slightly pedantic friend who reads over your shoulder and says, “You could do that… but you’ll hate yourself in the morning.”
This isn’t your grandma’s ESLint. The classic ESLint works on the level of Abstract Syntax Trees (ASTs)—it sees the shape of your code, but it’s blissfully unaware that string and number are different things. @typescript-eslint plugs directly into the TypeScript compiler’s type checker. This means its rules can leverage the full power of your type information to catch errors that are completely invisible to a vanilla linter. It’s the difference between a security guard checking for a visible ID badge and one who also runs a full background check.
The Non-Negotiable Setup
First, you need to install the squads. This isn’t a one-package deal.
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint
Now, your .eslintrc.js needs to be configured to hand off parsing duties to TypeScript and then use the plugin that contains all the type-aware rules.
// .eslintrc.js
module.exports = {
// Tell ESLint to use @typescript-eslint/parser instead of its default
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json', // This is the magic line. This gives the linter access to your type info.
tsconfigRootDir: __dirname,
},
plugins: [
'@typescript-eslint' // Enable the plugin that provides the rules
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended', // The sensible defaults
'plugin:@typescript-eslint/recommended-requiring-type-checking', // <- This one is key! This is where the type-aware rules live.
],
};
Notice the project option. This is the most common point of failure. If this path is wrong, the entire type-aware rule set simply shuts down and you’ll get a cryptic error. It needs to point to your tsconfig.json. Also, a pro tip: if you have a monorepo, you’ll likely need a separate ESLint config for each tsconfig.json.
Rules That Will Save Your Bacon
Here’s where the magic happens. Let’s look at rules that are only possible because the linter knows your types.
no-floating-promises
This one is a classic. It stops you from firing off a promise and forgetting to handle its rejection. Without type information, a linter just sees a function call. With it, the linter knows the function returns a Promise<void> and will yell at you for leaving it floating.
// This will trigger an error. You started a async task but didn't catch a potential failure.
async function fetchData() { /* ... */ }
fetchData(); // @typescript-eslint/no-floating-promises: Promises must be handled appropriately.
// The Fix: Either await it (if you're in an async function)...
await fetchData();
// ... or attach a .catch handler...
fetchData().catch((error) => console.error('Oops', error));
// ... or explicitly mark it as void if you're sure you want to fire-and-forget.
void fetchData();
await-thenable
This rule prevents one of the most face-palmy mistakes: trying to await something that isn’t a Promise. TypeScript’s type system knows what is and isn’t a Promise, so the linter uses that to stop you before you cause a runtime error.
// A simple function that returns a number
function getAnswer() {
return 42;
}
// This makes no sense, and now the linter will tell you that.
await getAnswer(); // @typescript-eslint/await-thenable: Only a Promise can be awaited.
Taming the Beast: Performance and Pitfalls
There is a cost to all this power. Running type-aware rules is slower because it effectively requires the linter to compile your entire project. For a large codebase, this can add significant time to your editor’s feedback loop or your CI process.
The solution? Be surgical. You can create a smaller tsconfig.eslint.json that only includes the files you actually want to lint. Your main tsconfig.json might include story files, tests, and scripts, but your linting config can be focused solely on your production source code. This dramatically reduces the number of files the linter has to analyze.
// tsconfig.eslint.json
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts" // Lint only the production source, not tests or stories
]
}
Then, just point your ESLint config to this new file: parserOptions: { project: './tsconfig.eslint.json' }.
The other big pitfall is getting the parser options right. If you see errors like “The file must be included in at least one of the projects provided,” it means ESLint is trying to lint a file that isn’t part of the tsconfig.json you pointed it to. Your choices are to include the file, ignore it with an .eslintignore, or use the parserOptions: { createDefaultProgram: true } flag as a blunt instrument (I don’t recommend this—it’s a performance hog and masks the real issue).
This tooling is what separates a TypeScript novice from a pro. It codifies best practices and catches entire classes of bugs at lint time, before they ever have a chance to run. It’s a bit fussy to set up, but once it’s running, you’ll wonder how you ever lived without it.