How to use ESLint with TypeScript

ESLint is a JavaScript linter that you can use to lint either TypeScript or JavaScript code. In this post, we'll walk through how to set up linting in your project.

Intro

Formatting is one of several concerns in the efforts to write clean code. There's a lot of other stuff we should be concerned about as well, but formatting is one of those things that we can set up right off the bat and establish a standard for our project.

ESLint and TSLint

ESLint is a JavaScript linter that enables you to enforce a set of style, formatting, and coding standards for your codebase. It looks at your code, and tells you when you're not following the standard that you set in place.

You may have also heard of TSLint, the TypeScript equivalent. In 2019, the team behind TSLint decided that they would no longer support it. The reason, primarily, is because ESLint exists, and there was a lot of duplicate code between projects with the same intended purpose.

That being said, there are some really awesome TSLint packages out there that, if you would like to use, you can- but just understand that they're not being supported anymore.

So onwards into 2020 and beyond, we're going to continue to look to ESLint for all of our TypeScript (and JavaScript) linting needs!

Prerequisites

Here's what you need to have in order to follow along:

Installation and setup

Run the following commands to setup ESLint in your TypeScript project.

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

Create an .eslintrc file.

touch .eslintrc

In it, use the following starter config.

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint"
  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended"
  ]
}

Ignoring files we don't want to lint

Create an .eslintignore in order to prevent ESLint from linting stuff we don't want it to.

touch .eslintignore

Then add the things we want to ignore. In the following code sample, we're ignoring the dist/ folder that contains the compiled TypeScript code. If you're compiling your TypeScript code to a different folder, make sure to use that instead of dist. You should be able to find this in your .tsconfig (see the previous guide).

node_modules
dist

Adding a lint script

In your project package.json, lets add a lint script in order to lint all TypeScript code.

{
  "scripts": {
    ...
    "lint": "eslint . --ext .ts",
  }
}

Ready to try it out? Let's run the following command.

npm run lint

For me, since I'm continuing with the previous tutorial, and since my src folder only has a single index.ts in it that prints out some text with console.log(), I don't see anything after I run the command.

src/index.ts
console.log('Hello world!')

What if we wanted to disallow console.log statements in our code?

Rules

There are three modes for a rule in eslint: off, warn, and error.

  • "off" means 0 (turns the rule off completely)
  • "warn" means 1 (turns the rule on but won't make the linter fail)
  • "error" means 2 (turns the rule on and will make the linter fail)

Adding a rule

In .eslintrc, add a new attribute to the json object called "rules".

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint"
  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "rules": { }}

Rules get added as keys of this rules attribute, and you can normally find the eslint base rules here on their website via the Rules docs.

We want to add the no-console rule, so here is an example of how we can make the linter fail (throw a mean error code) if it encounters a console.log statement with the no-console rule set to error.

We update the .eslintrc

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint"
  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "rules": { 
    "no-console": 2 // Remember, this means error!  }
}

And then run the linter again.

npm run lint

And we should get an angry linting result.

/simple-typescript-starter/src/index.ts

  2:1  error  Unexpected console statement  no-console
    ✖ 1 problem (1 error, 0 warnings)

And if we wanted, we could turn the rule off by setting it to 0 - off.

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint"
  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "rules": {     "no-console": 0 
  }
}

Run the linter.

npm run lint

...and silence.

Using rules in a real-life project

There's a reason that all three of these modes exist. When you set the linter to off for a rule, it's because you and your team probably don't care about that rule in a particular base configuration you're using (we're currently using the ESLint recommended config but you can also go and use Shopify's, Facebook's or several other companies' configs as starting points instead).

When you set the rule to error - 2, it means that you don't want the code that breaks your coding conventions to even make it the repo at all. I think this is a great act of professionalism and empathy towards the codebase, your fellow teammates, and future maintainers. A popular approach to actually enforce code conventions with this tool is to set up your project with a tool like Husky so that when a teammate tries to commit code or push code, you can tell your linter to check the code first before the operation executes. It's a great habit, though sometimes, if the rules are overly restrictive, it can slow down and annoy your teammates.

To remedy overly restrictive rules, the warn - 1 setting means that yes, you want you and your team to adhere to that rule, but you don't want it to prevent you from moving forward.

Adding a plugin (features)

ESLint also allows you to add one-off features to your config. These are known as plugins.

Here's a fun one. It's called no-loops.

Check out this list of other awesome-eslint plugins and configs.

no-loops is a plugin that will enable you to enforce a convention specifying that for and while loops are illegal and that you should use functions like map and forEach instead.

Install it like this.

npm install --save-dev eslint-plugin-no-loops

And then update your .eslintrc with no-loops in the "plugins" array, and add the rule to the "rules" attribute like so.

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint",
    "no-loops"  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "rules": {
    "no-console": 1,
    "no-loops/no-loops": 2  }
}

Now if we update our code to include a for loop...

src/index.ts
console.log('Hello world!');

for (let i = 0; i < 10; i++) {
  console.log(i)
}

And run the lint command again...

npm run lint

We'll see the errors that restricts loops appear.

/simple-typescript-starter/src/index.ts
  2:1   warning  Unexpected console statement                   no-console
  2:1   error    'console' is not defined                       no-undef
  4:1   error    loops are not allowed                          no-loops/no-loops  5:3   warning  Unexpected console statement                   no-console
  5:3   error    'console' is not defined                       no-undef
  5:17  error    Missing semicolon                              babel/semi
  5:17  error    Missing semicolon                              semi
  6:2   error    Newline required at end of file but not found  eol-last

✖ 8 problems (6 errors, 2 warnings)
  3 errors and 0 warnings potentially fixable with the `--fix` option.

Extending a different base config

Let's say that you wanted to start with a different base config- Shopify's, for example.

Here's how to do that.

Looking at the readme, we need to install it by running:

npm install eslint-plugin-shopify --save-dev

Update our .eslintrc

{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint"
  ],
  "extends": [
    "plugin:shopify/esnext"  ],
  "rules": {
    "no-console": 1
  }
}

You can add several base configs to your project by including them in the array, though you may end up seeeing the same linting rules twice or more.

Now when we run the lint command again with npm run lint, we can see a few errors reported based on the base Shopify config and our no-console rule.

/simple-typescript-starter/src/index.ts
  2:1   warning  Unexpected console statement  no-console
  2:1   error    'console' is not defined      no-undef
  2:28  error    Missing semicolon             babel/semi
  2:28  error    Missing semicolon             semi

✖ 4 problems (3 errors, 1 warning)
  2 errors and 0 warnings potentially fixable with the `--fix` option.

Fixing linted code with ESLint

You might have noticed that at the end of the error message, it says "2 errors and 0 warnings potentially fixable with the --fix option."

You can run ESLint and tell it to fix things that it's able to fix at the same time.

Using the --fix option, let's add a new script to our package.json called lint-and-fix.

{
  "scripts": {
    ...
    "lint": "eslint . --ext .ts",
    "lint-and-fix": "eslint . --ext .ts --fix"  },
}

Running this command against our code, I expect that it will put a semicolon on the console.log statement that we had.

Let's run it.

npm run lint-and-fix

The result is that less errors are reported. We don't see the error about semi-colons anymore.

/simple-typescript-starter/src/index.ts
  2:1  warning  Unexpected console statement  no-console
  2:1  error    'console' is not defined      no-undef

Because sure enough, the linter added the semi-colon.

src/index.ts
console.log('Hello world!');

That's really awesome. But what if we don't want to run the linter all the time to fix our code? What if there was a way that we could, while coding, have it automatically format things based on our conventions?

We can! With Prettier. Read the next article, "How to use Prettier with ESLint and TypeScript in VSCode".



Discussion

Liked this? Sing it loud and proud 👨‍🎤.


9 Comments

Submit
Sergio
7 months ago

A very useful and pragmatic post, thanks

qetr1ck
7 months ago

Thanks for sharing!

Would be great to see the best practice how to setup `eslint` with two more things

  • with `pre-*-git-hooks` via `husky`
  • with `prettier`

Karl Horky
7 months ago

Thanks for the guide Khalil!


I would suggest using the string values to configure rules though ("off", "warn", and "error"), documented here: https://eslint.org/docs/user-guide/configuring#configuring-rules

Waheed
7 months ago

Simple and straightforward. Thanks for this guide...already used it to setup a template.

ItayBS
5 months ago

Thank you very much! Great post!

kapil
3 months ago

Thanks, very much helpful.

Emmanuel Idun
2 months ago

Can it be installed globally?

Ilyas
2 days ago

When I run command I am getting:


$ eslint . --ext .ts


Oops! Something went wrong! :(


ESLint: 7.10.0


TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received type undefined
    at assertPath (path.js:39:11)
    at Object.join (path.js:434:7)
    at FileEnumerator._iterateFilesRecursive (E:\wertik-js\node_modules\eslint\lib\cli-engine\file-enumerator.js:426:35)
    at _iterateFilesRecursive.next (<anonymous>)
    at FileEnumerator.iterateFiles (E:\wertik-js\node_modules\eslint\lib\cli-engine\file-enumerator.js:287:49)
    at iterateFiles.next (<anonymous>)
    at CLIEngine.executeOnFiles (E:\wertik-js\node_modules\eslint\lib\cli-engine\cli-engine.js:751:48)
    at ESLint.lintFiles (E:\wertik-js\node_modules\eslint\lib\eslint\eslint.js:521:23)
    at Object.execute (E:\wertik-js\node_modules\eslint\lib\cli.js:294:36)
    at main (E:\wertik-js\node_modules\eslint\bin\eslint.js:142:52)
error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Ilyas
2 days ago

When I run command I am getting:


$ eslint . --ext .ts


Oops! Something went wrong! :(


ESLint: 7.10.0


TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received type undefined
    at assertPath (path.js:39:11)
    at Object.join (path.js:434:7)
    at FileEnumerator._iterateFilesRecursive (E:\wertik-js\node_modules\eslint\lib\cli-engine\file-enumerator.js:426:35)
    at _iterateFilesRecursive.next (<anonymous>)
    at FileEnumerator.iterateFiles (E:\wertik-js\node_modules\eslint\lib\cli-engine\file-enumerator.js:287:49)
    at iterateFiles.next (<anonymous>)
    at CLIEngine.executeOnFiles (E:\wertik-js\node_modules\eslint\lib\cli-engine\cli-engine.js:751:48)
    at ESLint.lintFiles (E:\wertik-js\node_modules\eslint\lib\eslint\eslint.js:521:23)
    at Object.execute (E:\wertik-js\node_modules\eslint\lib\cli.js:294:36)
    at main (E:\wertik-js\node_modules\eslint\bin\eslint.js:142:52)
error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.


Stay in touch!



About the author

Khalil Stemmler,
Developer Advocate @ Apollo GraphQL ⚡

Khalil is a software developer, writer, and musician. He frequently publishes articles about Domain-Driven Design, software design and Advanced TypeScript & Node.js best practices for large-scale applications.



View more in TypeScript



You may also enjoy...

A few more related articles

Client-Side Architecture Basics [Guide]
Though the tools we use to build client-side web apps have changed substantially over the years, the fundamental principles behind...
Absolute and Relative Complexity
Determining if the complexity of a problem is related to the nature of the problem or related to the way that we approach solving ...
How to Deploy a Serverless GraphQL API on Netlify [Starters]
Exploring two starter projects you can use to deploy a serverless GraphQL API on Netlify with or without TypeScript.
How to Get the Currently Playing Song using the Spotify Node.js API & TypeScript
A quick and straightforward guide to hooking into the Spotify's awesome API to show the current song that you're listening to. It ...

Want to be notified when new content comes out?

Join 5000+ other developers learning about Domain-Driven Design and Enterprise Node.js.

I won't spam ya. 🖖 Unsubscribe anytime.

Get updates