Configuring ESLint, Prettier, and TypeScript Together
May 01, 2023
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:
- Formatters (e.g. Prettier): which only format your code quickly, without worrying about logic
- 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
- 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 YouTubePhoto credit Rebecca Bakels. See the PowerPoint slides here.
Resources
Everything in this blog post is available online for free:
- My talkâs recording and accompanying talk slides
- template-typescript-node-package: Template repo I maintain that sets up these three tools (and more!) in a general Node package
- Linting TypeScript in 2023: Demo repo showing using three type-checked typescript-eslint rules to catch three bugs in a React app
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": {
"operator": "||",
"type": "LogicalExpression",
"left": "friend",
"right": "\"me\""
}
},
"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
Formatters clean your code. That's all they do. [image source]
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
me irl
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:
-
"off"
: it shouldnât be run at all -
"warn"
: its complaints should show up as warnings (yellow squigglies), and shouldnât cause ESLint to exit with a non-zero status code (i.e. not failing the build) -
"error"
: its complaints should show up as errors (red squigglies), and should cause ESLint to exit with a non-zero status code (i.e. failing the build)
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:
- Install all plugins relevant to your project
- Extend from each of the pluginsâ recommended configs
-
In your ESLint config:
- Disable any lint rules that you know you donât want, and explain why
- 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
for all of React, and
eslint-plugin-react-hooks
for hooks specifically.
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"
}
}
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 set it as your default formatter and 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!
- A formatter reformats all in one pass, which means itâll be faster than a linter but wonât look for logical issues.
- A linter runs a set of discrete rules, which means itâs slower than a formatter but can find & sometimes fix logical issues.
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!
me and all other linter maintainers irl having to deal with formatting rules [image source]
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:
- Programming language: A description of a language whose syntax includes everything in JavaScript and some new types-specific stuff
- Type Checker: A program that reads in your files and reports on mismatches between the codeâs intent and how it will execute
- Compiler: A program that runs the type checker, as well as transpiling TypeScript source code into the equivalent JavaScript
- Language Service: A program that runs the compiler and/or type checker inside an editor/IDE such as VS Code
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:
-
"jsx"
: Indicates that TypeScript should allow JSX syntax -
"module"
: Which module system TypeScript should assume code is running in -
"strict"
: Enables a suite of useful opt-in type checking rules that make TypeScript more strict (so itâll catch more issues) -
"target"
: Indicates which global APIs & environment syntax features TypeScript should assume are available
A few more compiler options can also useful in many projects:
-
"esModuleInterop"
: Tells TypeScript to be less nitpicky about importing between CommonJS vs. ESM modules (as itâs quite pedantic by default) -
"forceConsistentCasingInFileNames"
: Enables TypeScript letting you know if you typo an import by using the wrong casing -
"skipLibCheck"
: Prevents TypeScript from spending time type checkingnode_modules
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!
- A traditional linter runs a set of discretely configurable rules that only see one file at a time, which means itâs faster and more configurable than a type checker.
- A type checker builds a full understanding of all files, which means itâs slower than a traditional linter but more powerful in what it can deduce.
You can think of the difference as being that:
- Type checkers let you know when the code blatantly doesnât make sense (e.g. providing a numeric value in a place that should only receive strings)
-
Linters let you know when the code makes sense, but is
probably wrong (e.g. calling a function marked as
@deprecated
)
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:
-
@typescript-eslint/eslint-plugin
: provides lint rules and preset configurations tailored to TypeScript code -
@typescript-eslint/parser
: tells ESLint how to read TypeScript syntax
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,
+ },
};
-
plugin:@typescript-eslint/recommended-requiring-type-checking
is our recommended config that additionally enables type-aware rules -
parserOptions.project
is required for typescript-eslint to know which TSConfig contains the compiler options to generate type information with;true
indicates to use the closest one to each source file
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:
- Formatters such as Prettier format your code quickly, without worrying about code logic
- Linters such as ESLint run a set of discrete rules on your code logic
- Type Checkers such as TypeScript build an understanding of your project and error when stated intentions are violated
- typescript-eslint allows ESLint to parse TypeScript code and exposes TypeScript type checking APIs to ESLint rules
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:
- https://eslint.org/donate
- https://opencollective.com/prettier
- https://opencollective.com/typescript-eslint
Remember: sponsorships are how open source projects are able to keep development going! â¤ď¸