Back to Blog
TypeScript and ESLint: Stop Guessing, Start Enforcing
7 min readJun 27, 20261 views

TypeScript and ESLint: Stop Guessing, Start Enforcing

It's easy to assume TypeScript catches everything, but ESLint still plays a crucial role. Learn how typescript-eslint supercharges your linting with type-aware rules, preventing subtle bugs and enforcing better patterns.

TypeScriptToolingFrontendSoftware Design
Share

by Sunil Band

Type-Checking Isn't Enough

We've all been there: you migrate a project to TypeScript, add a few anys to get it compiling, and breathe a sigh of relief. "Great, type safety!" you think. Then a week later, a runtime error pops up that TypeScript should have caught. Or, more commonly, a junior engineer pushes some code that compiles fine but violates every clean code principle you've ever preached. TypeScript is phenomenal, don't get me wrong, but it only ensures type correctness. It doesn't enforce code style, best practices, or prevent logical pitfalls that still result in valid JavaScript, even if that JavaScript is terrible.

This is where typescript-eslint comes in. It bridges the gap between TypeScript's powerful type analysis and ESLint's flexibility in enforcing code quality. It lets ESLint understand your types, unlocking a whole new class of rules that vanilla ESLint or even tsc on its own can't provide. If you're running a TypeScript project without it, you're leaving a massive amount of code quality on the table.

Why Type-Aware Linting Matters

Traditional ESLint rules operate solely on the Abstract Syntax Tree (AST) of your code. They see const x = "hello"; and can tell you if you used single quotes or double quotes, or if x should be const or let. But they have no idea that x is a string if you haven't explicitly typed it, and even if you have, they can't infer complex types or relationships between different parts of your codebase.

TypeScript, on the other hand, builds up a rich semantic model of your entire project, understanding every type, every interface, and every possible relationship. typescript-eslint taps into this exact semantic model. This allows ESLint rules to go beyond just syntax and enforce things based on actual type information. Think about it: preventing unhandled Promise rejections, ensuring exhaustive switch statements over union types, or disallowing certain function calls based on the runtime type of an argument. These are things you simply can't do without type awareness.

Getting Started

If you're already using ESLint with TypeScript, the migration is usually straightforward. If not, you'll need to install ESLint first. Assuming you have a package.json:

bash
npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser

Next, you need to configure your .eslintrc.json (or similar config file) to use the @typescript-eslint/parser and include the plugin. Here's a basic setup:

json
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module",
    "project": "./tsconfig.json" // Crucial: tells eslint where your tsconfig is for type-aware rules
  },
  "plugins": [
    "@typescript-eslint"
  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended", // Recommended type-aware rules
    "plugin:@typescript-eslint/recommended-type-checked" // Even stricter type-aware rules
  ],
  "rules": {
    // Your custom rules here
    "@typescript-eslint/no-explicit-any": "warn", // Example: warn on 'any'
    "@typescript-eslint/consistent-type-imports": "error" // Example: enforce type-only imports
  }
}

Pay close attention to "project": "./tsconfig.json". This line is absolutely critical for type-aware rules to work. Without it, typescript-eslint can't build the type graph and those advanced rules will be disabled or won't function correctly. If you have multiple tsconfig.json files (e.g., for different workspaces in a monorepo), you can provide an array of paths.

Integrating with React/Next.js

If you're in a React or Next.js project, you'll want to extend some additional configurations. For example, for React:

json
{
  "extends": [
    // ... other extends
    "plugin:react/recommended",
    "plugin:react-hooks/recommended"
  ],
  "settings": {
    "react": {
      "version": "detect" // Automatically detect React version
    }
  },
  "rules": {
    // ... other rules
    "react/react-in-jsx-scope": "off" // For React 17+ where JSX transform doesn't require importing React
  }
}

And if you're using Next.js, you'll likely want to extend their recommended config too:

json
{
  "extends": [
    // ... other extends
    "next",
    "next/core-web-vitals"
  ]
}

This setup provides a solid baseline. Now you can start adding specific type-aware rules that will make a real difference.

Powerful Type-Aware Rules to Try

Here are some of my favorite rules that you can only get with typescript-eslint and which I consider essential for robust TypeScript projects:

no-floating-promises

This rule is a lifesaver. It flags Promises that are created but not handled (either awaited, .then()'d, or .catch()'d). Unhandled promises can lead to silent errors or unexpected behavior, especially in asynchronous code. You'd be surprised how often this happens, even with experienced developers.

typescript
async function fetchData(): Promise<string> { return 'data'; }

// Bad: Promise is created but not handled
fetchData(); // ESLint will flag this!

// Good: Promise is awaited
async function processData() {
  await fetchData();
}

// Good: Promise is explicitly handled
fetchData().catch(error => console.error(error));

switch-exhaustiveness-check

If you use union types and switch statements, this rule is a godsend. It ensures that every possible case in a union type is handled in your switch statement. If you later add a new variant to your union, ESLint will immediately tell you where you need to update your switches. This is a huge win for maintainability and preventing runtime errors from unexpected states.

typescript
type EventType = 'CLICK' | 'HOVER' | 'DRAG';

function handleEvent(event: EventType) {
  switch (event) {
    case 'CLICK':
      console.log('Clicked');
      break;
    case 'HOVER':
      console.log('Hovered');
      break;
    // case 'DRAG': // If I uncomment this, ESLint will complain if 'DRAG' is missing!
    //   console.log('Dragged');
    //   break;
    default:
      // This 'default' case will be flagged if not all union members are handled
      // unless you explicitly type `event` as `never` in the default case to tell TypeScript
      // that this path should literally never be reached if all cases are handled.
      const _exhaustiveCheck: never = event; // This is a common pattern to ensure exhaustiveness
      return _exhaustiveCheck;
  }
}

no-unsafe-assignment, no-unsafe-call, no-unsafe-member-access, no-unsafe-return

These rules are part of the strict-type-checked configuration and are fantastic for gradually eliminating any from your codebase. They prevent unsafe operations on values that have an any type. While it's sometimes necessary to use any as a temporary escape hatch, these rules make sure you're aware of the risks and push you towards more specific types. They force you to be explicit when you're bypassing type safety.

typescript
function processUnknown(data: unknown) {
  // ESLint will flag these as unsafe because `data` is unknown, not any.
  // You must narrow the type first.
  // const myValue: any = data; // If you intentionally assign to any, these rules will then flag subsequent usage
  // myValue.someMethod(); // flagged as no-unsafe-call

  if (typeof data === 'object' && data !== null && 'value' in data) {
    const safeValue = (data as { value: string }).value;
    console.log(safeValue);
  }
}

These are just a few examples. The typescript-eslint plugin offers a vast array of rules that can significantly improve your code quality and catch bugs that TypeScript's compiler might miss or ignore due to configuration.

The Cost of Power: Performance and Configuration

There's no free lunch, and type-aware linting is no exception. Because typescript-eslint needs to build the full TypeScript program to perform its analysis, it's inherently slower than plain ESLint. For smaller projects, this might be negligible, but in large monorepos with hundreds of thousands of lines of code, linting can take several seconds or even tens of seconds.

This performance hit is the main trade-off. However, in my experience, the benefits of catching subtle type-related issues and enforcing stricter patterns far outweigh the increased linting time. Most modern IDEs integrate ESLint well, often running it in the background or on file save, making the perceived performance impact less severe during development. For CI/CD, you'll just have to accept that your linting step will take longer.

Another point to consider is configuration complexity. The project option in parserOptions needs to correctly point to your tsconfig.json files. In complex monorepos, this can sometimes be tricky to set up correctly, especially if you have nested tsconfig files or rely on project references. You might need a separate tsconfig.eslint.json that extends your main tsconfig.json but includes all files that ESLint should process, potentially avoiding skipLibCheck issues if you're trying to lint your node_modules (which you generally shouldn't).

Wrapping up

If you're serious about code quality and maintainability in your TypeScript projects, typescript-eslint is not optional; it's essential. It supercharges your ESLint setup with the deep type information provided by TypeScript, allowing you to catch an entirely new class of errors and enforce stricter coding standards. Don't rely solely on tsc to be your quality gate; it's a compiler, not a style or best practice enforcer.

Your next step should be to integrate @typescript-eslint/recommended-type-checked into your .eslintrc.json and then iteratively enable the strict-type-checked rules. Start with a few rules, fix the violations, and then add more. It's an investment that pays dividends in fewer bugs and more readable code. Head over to the typescript-eslint documentation and start exploring the rules that make sense for your team and project.

More from the blog
Available for projectsReady to make something fun 🎈

Ready to build the next system?Wanna build something awesome together?

Currently accepting high-impact opportunities in frontend engineering and scalable web applications.Got a cool idea rattling around? Let's grab a virtual coffee and turn it into something people love. ☕