Josh Goldberg
Josh smiling on stage and giving a presentation showing the "Type Checking React (v2)" from the linked slides.

Configuring ESLint, Prettier, and TypeScript Together

May 1, 202330 minute read

How I recommend getting your formatter, linter, and type checker to play together nicely.

Static analysis is tooling that scrutinizes code without running it. This is in contrast with dynamic analysis: tooling such as testing that executes your code and scrutinizes the result. Static analysis tools tend to exist on a spectrum from speed to power:

  1. Formatters (e.g. Prettier): which only format your code quickly, without worrying about logic
  2. Linters (e.g. ESLint): which run a set of discrete rules meant to check the raw logic of your code, one file at a time
  3. Type checkers (e.g. TypeScript): which generate an understanding of all your files at once and validate that code behavior matches intent

I recently gave a talk at React Miami 2023 about setting up ESLint and TypeScript for React that includes my recommendations. This blog post covers all the info in that talk: describing how to get started with each form of static analysis in JavaScript/TypeScript and some quick tips for using them effectively.

You can watch my talk along with all the other React Miami talks here on YouTube.

Photo credit Rebecca Bakels. See the PowerPoint slides here.

Resources

Everything in this blog post is available online for free:

I’ve also posted a separate FAQs article for assorted questions.

Abstract Syntax Trees (ASTs)

Before we dig into the tools, I want to briefly mention Abstract Syntax Trees (ASTs).

An AST is an object description of your source code’s contents. Static analysis tools generally read your source files into an AST to be able to understand your code.

For example, code like friend = friend || "me" could be represented with something like:

{
	"expression": {
		"left": "friend",
		"operator": "=",
		"right": {
			"left": "friend",
			"operator": "||",
			"right": "\"me\"",
			"type": "LogicalExpression"
		}
	},
	"type": "AssignmentExpression"
}

If you’re curious how TypeScript ASTs work, you can read about them on ASTs and typescript-eslint and play around with them on typescript-eslint.io/play.

The concept of ASTs sometimes shows up in tool documentation - and while you don’t need to understand ASTs to use static analysis tools, they’re a useful concept in general. Just know that when someone says AST, they’re talking about how tools represent your code.

Enough theory! Let’s dig into the types of tools.

Formatting

A formatter is a tool that reads in your source code, ignores your formatting, and suggests how to write it. For example, given this oddly formatted code block:

friend =  friend
    || "me"

…a formatter might suggest rewriting it like so:

friend = friend || "me";

Note that the formatter didn’t change the logic of the code. It just cleaned it up visually to be easier to read. Which is wonderful - using formatters, we don’t have to manually format files ourselves!

Prettier is the most common formatter in web apps today. You can get started using it by installing it as a dependency, then running it with --write on . (the current directory) to auto-format all your files:

npm install prettier --save-dev
npx prettier . --write

I’d encourage you to read the Prettier docs and Prettier installation guide in particular for more details.

Editor Formatting Settings

I’d also encourage you to enable the Prettier extension for VS Code in your .vscode/extensions.json workspace recommendations:

// .vscode/extensions.json​
{​
  "recommendations": ["esbenp.prettier-vscode"]​
}

…then set it as your default formatter and enable formatting on save in your .vscode/settings.json workspace settings:

// .vscode/settings.json​
{​
  "editor.defaultFormatter": "esbenp.prettier-vscode",​
  "editor.formatOnSave": true​
}

That way, every time you save a file or run the VS Code Format Document command, Prettier will completely format your document for you. That means you don’t have to fix up spaces, newlines, etc. manually!

Prettier also has configuration options. But, I generally avoid most of them and go with the default recommendations. As long as my formatting is consistent, I don’t sweat the details. The only option I generally set in my configs is changing useTabs to true:

// .prettierrc.json
{
	"useTabs": true
}

If you want much more control over your formatting, you might prefer dprint for formatting. It’s much more configurable than Prettier, though less widely used.

Linting

A linter is a tool that runs a set of checks on your source code. Modern linters such as ESLint, the standard linter for JavaScript, generally set those up to be discrete rules (they run independently and don’t have any visibility into which other rules are enabled). Each rule may report on code it doesn’t like, and each complaint may contain an optional autofix.

For example, if you enabled the ESLint logical-assignment-operators rule on the snippet from Formatting, you’d receive a message and suggested fix like:

- friend = friend || "me";
+ friend ||= "me";
Assignment (=) can be replaced with operator assignment (||=).

You can see the ESLint playground showing the logical assignment complaint.

To get started locally with ESLint, you can install it as a dependency, run its initializer to create a starter config, and run ESLint on your current directory (.):

npm install eslint --save-dev
npm init @eslint/config

npx eslint .

ESLint Configurations

Your ESLint configuration file is a description of all the ESLint plugins (npm packages that add additional rules or other linting behavior) and configuration options for rules you want to enable or disable. Each rule can be set to one of three severities:

Manually configuring each and every rule you’d want to enable would be a lot of work - a lot of projects enable hundreds of rules! Instead, ESLint allows configurations to extend from preset configs that do the work of choosing & configuring rules for you. I strongly recommend at the very least extending from ESLint’s eslint:recommended config, which contains rules the ESLint team has found to be desirable for the vast majority of JavaScript projects:

// .eslintrc.js
module.exports = {
	extends: "eslint:recommended",
};

Granular Rule Configuration

You can always disable ESLint rules that aren’t useful for you, or have too many errors. That’s right - it’s ok to disable a lint rule! The linter is a tool like any other, and it should be configured to match your needs.

My general recommendation for configuring ESLint is to:

  1. Install all plugins relevant to your project
  2. Extend from each of the plugins’ recommended configs
  3. In your ESLint config:
    1. Disable any lint rules that you know you don’t want, and explain why
    2. Also disable any lint rules that you do want but don’t have time to enable yet, with a tracking issue/ticket filed to enable eventually
// .eslintrc.js
module.exports = {
	extends: "eslint:recommended",
	rules: {
		// These rules are enabled by default, but we don't want
		"some-annoying-rule": "off", // (conflicts with XYZ preference)

		// Todo: these rules might be useful; we should investigate each
		"powerful-rule": "off", // (#123)
	},
};

Configuring ESLint for React (Or Similar)

You can ignore this section if you don’t work in a frontend framework that uses JSX or other nonstandard JavaScript dialects.

Since the linked talk was given at a React conference, it also showed configuring ESLint for React. Any project that uses JSX with vanilla JavaScript needs to set parserOptions.ecmaFeatures.jsx to true so that ESLint’s parser knows to allow JSX. There are two commonly used plugins for React:

eslint-plugin-react additionally asks to configure settings.react.version so it knows which React-version-specific behavior to run with.

// .eslintrc.js
module.exports = {
  extends: [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
  ],
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
  },
  plugins: ["react", "react-hooks"],
  settings: {
    react: {
      version: "detect",
    },
  },
};​

Other frameworks/libraries have their own plugins, including eslint-plugin-astro, eslint-plugin-solid, etc.

More Linting Flags

I generally recommend enabling two more flags in ESLint runs.

Reporting Unused Disable Directives

“Disable directives” are comments in code that disable some or all ESLint rule(s) for a particular area of code. For example, this block disables no-console for a single log:

// eslint-disable-next-line no-console
console.log("Hello, world!");

ESLint by default won’t warn you if you leave those comments in places that don’t need them:

// eslint-disable-next-line no-console
myFancyLogger("Hello, world!");

--report-unused-disable-directives causes ESLint to treat unnecessary disable directives the same as a complaint from an actual lint rule.

// package.json
{
	"scripts": {
		"lint": "eslint . --report-unused-disable-directives"
	}
}

Edit (January 2024): reportUnusedDisableDirectives: true, is now a configuration option you can set in your ESLint config file! create-typescript-app has been updated.

Fun fact: I authored the PR that enabled --report-unused-disable-directives violations to be fixed by --fix. Hooray, open source!

Warnings Maximum

Rules with severity set to "warn" don’t cause ESLint to fail the build. In my experience, leaving rules as warnings instead of errors allows them to build up over time, which trains users to ignore them. I recommend only using "warn" temporarily for newly enabled rules, and generally configuring all rules as "error" when possible.

See this interesting ESLint discussion on what a warning vs. an error means.

--max-warnings allows you to specify a maximum number of warnings that are permitted. If ESLint receives more warning-level rule complaints than that number, it’ll switch to existing with an error code.

I recommend keeping that number as low as possible, preferably 0:

// package.json
{
	"scripts": {
		"lint": "eslint . --max-warnings 0"
	}
}

Editor Linting Settings

I’d also encourage you to enable the ESLint extension for VS Code in your .vscode/extensions.json workspace recommendations:

// .vscode/extensions.json​
{​
  "recommendations": ["dbaeumer.vscode-eslint"]​
}

…then enable linting on save in your .vscode/settings.json workspace settings:

// .vscode/settings.json​
{​
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}

That way, every time you save a file or run the VS Code ESLint: Fix All Auto-Fixable Problems command, ESLint will --fix any fixable problems for you. Which is particularly useful if you use plugins like eslint-plugin-simple-import-sort (which I highly recommend!).

STOP USING ESLINT FOR FORMATTING

A formatter is not a linter! A linter is not a formatter! The two types of tools work differently on the inside. They’re not the same!

Although a linter can have rules tailored to formatting (e.g. indent, max-len, semi), those rules get to be ridiculously complex and difficult to maintain because of all the edge cases they need to handle. In typescript-eslint land we’ve given up on the indent rule altogether. Formatting rules are evil, a waste of time to maintain, and not the right way to format your code. Use a dedicated formatter, please!

Type Checking

Today, TypeScript is the standard type checker for JavaScript. People like to describe TypeScript as a “superset of JavaScript” (a.k.a. “JavaScript with types”). But the word “TypeScript” really refers to four things provided by the TypeScript team:

TypeScript is useful because there’s no standard built-in way in JavaScript to describe the intent behind code. For example, this JavaScript snippet declares a variable but never explains what type of values it’s allowed to contain:

let myValue; // what is this?!

TypeScript type annotation syntax would allow describing its intent (what type of value it’s allowed to store):

let myValue: number;

…and the TypeScript type checker can warn us if we assign something to that variable that doesn’t match our stated intent:

myValue = "not a number";
// Error: Type 'string' is not assignable to type 'number'.

To get started locally with TypeScript, you can install it as a dependency, run its init command to create a starter config, and run it:

npm install typescript --save-dev
npx tsc --init

npx tsc

TypeScript Configuration

tsc --init will give you a good starting config. The following compiler options are the minimum I recommend for most projects using React or another framework that uses JSX:

A few more compiler options can also useful in many projects:

aka.ms/tsconfig explains each of the available compiler options. I also explain many of them more deeply in Learning TypeScript > Chapter 13: Configuration Options.

Note that compiler options are generally set for you by framework starters such as create-next-app. As long as they set strict: true, you should be fine.

TypeScript Editor Configuration

I generally recommend the following settings in your .vscode/extensions.json workspace recommendations:

// .vscode/settings.json
{
	"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
	"typescript.tsdk": "node_modules/typescript/lib"
}

"eslint.rules.customizations" tells VS Code to visualize all ESLint rule complaints as yellow (warning) squigglies instead of the matching squiggly color for their configured severity. I’ve found this to be useful because TypeScript complaints are generally surfaced as red (error) squigglies. It can be confusing having two tools surface complaints with the same color. Showing TypeScript complaints in red and ESLint complaints in yellow helps folks understand which is which.

"typescript.tsdk" tells VS Code that it should use the project’s installed TypeScript package for IDE tooling, rather rather than your computer’s global VS Code / TypeScript install. This is good because the project might have a different version of TypeScript installed than your VS Code. You wouldn’t want to have potentially different TypeScript results from running tsc on the terminal vs. from VS Code’s language services.

Fun fact: I authored the PR that added "eslint.rules.customizations" to the VS Code ESLint extension. Hooray, open source!

TypeScript Is Not A Linter

Don’t confuse linting with type checking. The two are not the same!

You can think of the difference as being that:

I say traditional linter because later we’ll see how to enable powerful lint rules that make use of TypeScript’s APIs.

Linting TypeScript Code

ESLint by default only understands JavaScript syntax, not the new syntax included in TypeScript. Its core rules don’t lint for TypeScript-specific logic or best practices. That’s why typescript-eslint provides two packages for ESLint users:

Prettier also uses typescript-eslint internally, which is how it supports TypeScript syntax out-of-the-box.

typescript-eslint.io includes a Getting Started guide for linting TypeScript code. The most straightforward linter config I can suggest using utilizes those two packages to extend from both ESLint’s recommended rules as well as plugin:@typescript-eslint/recommended, the equivalent starter config for TypeScript:

// eslintrc.js
module.exports = {
	extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
	parser: "@typescript-eslint/parser",
	plugins: ["@typescript-eslint"],
};

For example, the @typescript-eslint/prefer-as-const rule can inform developers of using as const instead of retyping literal values in type assertions:

- let me = { name: "ReactMiami" as "ReactMiami" };
+ let me = { name: "ReactMiami" };

Type Aware Rules

Traditional lint rules only see one file at a time. typescript-eslint provides APIs that allow rules to tap into TypeScript’s type checker - thereby making a classification of much more powerful lint rules. These “type aware” lint rules are not enabled in plugin:@typescript-eslint/recommended because they’re much slower than traditional lint rules, as they run at the speed of type checking (which needs to run on your entire project).

typescript-eslint’s Linting with Type Information guide shows what you need to do to enable these rules. Minimally:

module.exports = {
    extends: [
        "eslint:recommended",
        "plugin:@typescript-eslint/recommended",
+        "plugin:@typescript-eslint/recommended-requiring-type-checking",
    ],
    plugins: ["@typescript-eslint"],
    parser: "@typescript-eslint/parser",
+    parserOptions: {
+        project: true,
+    },
};

For example, @typescript-eslint/await-thenable reports on any await used on a statement that isn’t a Thenable such as a Promise.

- await console.log("wat");
+ console.log("wat");
// Unexpected await of a non-Promise (non-"Thenable") value.

Type-aware lint rules with typescript-eslint are, to my knowledge, the most powerful lint rules you can get for JavaScript/TypeScript projects today. I’d highly recommend using them!

Putting it All Together

Let’s recap how the tools all interact:

Linting TypeScript in 2023: is a demo repo showing configurations for all those tools, as well as an example of using three type-checked typescript-eslint rules to catch three bugs in a React app.

See also the separate FAQs article for assorted questions on static analysis. This includes any questions you might have about plugins like eslint-config-prettier, eslint-plugin-prettier, tslint, tslint-config-prettier, and tslint-plugin-prettier.

Supporting Open Source Projects

ESLint, Prettier, and typescript-eslint are all independent open source projects. That means their development is supported by community donations rather than any single company or company team. All three projects, like most open source projects, are underfunded and would absolutely appreciate your support:

Remember: sponsorships are how open source projects are able to keep development going! ❤️


Liked this post? Thanks! Let the world know: