Mountain Top

A blog for the TypeScript developer ecosystem

Learn TypeScript Linting Part 1

Written by: Ethan Arrowood

TypeScript and ESLint

This guide will show you how to set up ESLint with a TypeScript project. The guide is broken up into three sections. The first two are about setting up ESLint and configuring it to work with TypeScript. The last section contains additional context and a list of resources for those interested in learning more. Make use of the table of contents and the [toc] shortcuts to better navigate this article.

Part 2 of this guide covers integrating popular styling formatters Standard and Prettier.

Table of Contents

1 Getting Started

[toc]

Before starting this article, review the following prerequisite information:

  • Installed, stable version of Node.js and the accompanying version of npm. At the time of writing/publishing this article this includes all stable Node.js versions in scope of: v8.x, v10.x, v11.x, or v12.x.
  • Installed, stable version of git
  • A bash terminal. Mac and Linux users should use the default Terminal application. Windows users should use Git Bash or another bash emulator. The Window’s Subsystem for Linux is also a great option. The commands used in the article are bash commands and are not verified to work on non-bash terminals such as Powershell.

Note: Developers without an existing TypeScript project should start here at section 1; developers with an existing project can skip ahead to section 2 Adding ESLint. This guide works best if you follow along with the GitHub repository.

View the GitHub repository learn-typescript-linting. Copy, paste and execute the following command to clone it to your machine:

git clone https://github.com/MatterhornDev/learn-typescript-linting.git
cd learn-typescript-linting
git checkout init
npm install

The repository comes with multiple branches for different points in the guide.

The project comes with a single developer dependency, typescript, and two npm scripts, compile and start. The compile command is tsc -p tsconfig.json. The project is configured for es5 in strict mode and includes all .ts files under the src directory. The compiled output can be found in the lib directory. The start command runs the compiled .js output via node lib/index.js. Try them out by running:

npm run compile

# Result:
> learn-typescript-linting@1.0.0 compile /learn-typescript-linting
> tsc -p tsconfig.json
npm run start

# Result:
> learn-typescript-linting@1.0.0 start /learn-typescript-linting
> node lib/index.js

2 ** 8 = 256
35

It is recommended you do not modify the .ts files as they are specifically set up to show off unique aspects of linting typescript projects.

2 Adding ESLint

[toc]

Install eslint, @typescript-eslint/eslint-plugin, @typescript-eslint/parser as developer dependencies. Initialize an empty eslint configuration file. I prefer to use .json for configuration files.

npm i -D eslint @typescript-eslint/{eslint-plugin,parser}
touch .eslintrc.json

2.1 Initializing .eslintrc

[toc]

Add the following to .eslintrc.json

{
  "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "env": { "node": true },
  "parserOptions": {
    "ecmaVersion": 5,
    "sourceType": "module"
  },
  "rules": {}
}

2.1.1 Specifying environments

[toc]

The env object is used for defining global variables in a project that are not explicitly imported. A great example is the console object. This is considered globally available in both browser and Node.js environments. In the learn-typescript-linting example, the code will be executed in a Node.js terminal, thus the node attributes is enabled. Some other common attributes include jest, mocha, amd, commonjs, and es6. There is no easy way to know which attributes need to be enabled; it is recommended to consult ESLint’s Specifying Environments documentation to find out what each environment attribute provides.

2.1.2 Specifying ecmaVersion

[toc]

The parserOptions.ecmaVersion value is based on the target value found in the tsconfig.json. A tsconfig.json value of { "target": "es5" } is equivalent to { "ecmaVersion": 5 }. Use the table below for additional mappings.

tsconfig.json target .eslintrc.json ecmaVersion
es3 3
es5 5
es6 or ES2015 6 or 2015
ES2016 7 or 2016
ES2017 8 or 2017
ES2018 9 or 2018
ES2019 10 or 2019
ESNext 10 or 2019

Take a look at TypeScript’s --lib compiler options to learn how to include unique library files in the compilation. By setting target to either es5 or es6, TypeScript will automatically import a set of libraries (i.e. { target: es5 } = { lib: ['DOM', 'ES5', 'ScriptHost']}).

2.2 Creating ESLint npm script

[toc]

Now that ESLint is configured, create a new npm script in package.json by adding it to the "scripts" object:

{
  "scripts": {
    "compile": "tsc -p tsconfig.json",
    "start": "node lib/index.js",
+   "lint": "eslint 'src/**/*.ts'"
  }
}

This command will run ESLint on all .ts files within the src directory. The /**/* glob pattern will reach all files within subdirectories of src. If you have multiple directories to run the linter on (such as a test directory), use a pattern such as: {src,test}/**/*.ts.

2.3 Executing ESLint

[toc]

Run the new command via npm run lint, you should get an output similar to:

> learn-typescript-linting@0.1.0 lint /learn-typescript-linting
> eslint 'src/**/*.ts'

/learn-typescript-linting/src/bar.ts
  4:1  error  Expected indentation of 4 spaces but found 2  @typescript-eslint/indent
  4:19  warning  Missing return type on function            @typescript-eslint/explicit-function-return-type

/learn-typescript-linting/src/foo.ts
  4:1  error  Expected indentation of 4 spaces but found 2  @typescript-eslint/indent
  5:1  error  Expected indentation of 4 spaces but found 2  @typescript-eslint/indent

/learn-typescript-linting/src/index.ts
   9:1  error  Unexpected console statement  no-console
  10:1  error  Unexpected console statement  no-console

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

npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! learn-typescript-linting@0.1.0 lint: `eslint 'src/**/*.ts'`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the learn-typescript-linting@0.1.0 lint script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /.npm/_logs/2019-05-05T00_51_14_059Z-debug.log

Note future command-output references in this post will not include the first two lines (denoted by the > character) nor the npm ERR! lines as they are not important and contain no useful information (for us). The npm ERR! occurs when npm run lint fails with a linting error. If the script returns a warning, an npm error is not thrown. This is expected behaviour and is mainly used in a continuous integration pipline (stay tuned for a future post on CI pipelines).

The output shows two main things. First, it outputs linting errors and warnings (i.e. @typescript-eslint/indent and no-console). These will come in handy for further configuring ESLint. Second, it says ” 3 errors and 0 warnings potentially fixable with the --fix option.”. ESLint comes with a great CLI option --fix to automatically fix certain linting errors and warnings.

Run npm run lint -- --fix to pass the --fix option down to the eslint command. After it completes there should be a new output:

/learn-typescript-linting/src/bar.ts
  4:21  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type

/learn-typescript-linting/src/index.ts
   9:1  error  Unexpected console statement  no-console
  10:1  error  Unexpected console statement  no-console

✖ 3 problems (2 errors, 1 warning)

The source files have now been updated to use 4-spaces per indent rather than 2. Section 2.8, will describe how to change this configuration back to 2-spaces as well as how to enable semicolons.

2.4 Fixing an ESLint warning

[toc]

TypeScript exists to help developers write better and safer code. The typescript-eslint package helps accomplish this by warning of missing explicit types. The warning:

/learn-typescript-linting/src/bar.ts
  4:21  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type

refers to the missing return type from the arrow function inside the .reduce method in the bar.ts file. Modify the code by specifying a return type:

import { CustomType } from './foo'

export function bar (a: CustomType, b: CustomType[]): CustomType {
-    return b.reduce((c, v) => c+=v, a)
+    return b.reduce((c, v): CustomType => c+=v, a)
}

Run the linter (npm run lint), observe how the previous warning no longer exists!

2.5 Configuring ESLint

[toc]

To start, configure the no-console rule by adding the following "rules" object to the .eslintrc.json

{
  "rules": {
+   "no-console": "warn"
  }
}

The no-console rule can be configured to one of three values: "error" (default), "warn", or "off". Specify the rule to whichever value best serves the project at hand. In my opinion, no-console should be enabled as a warning because in a production application it is considered best-practice not to log to console, but to instead use a legitimite logger such as Pino.

2.6 Additional ESLint rule configuration

[toc]

Some other opinionated rules are indent spacing and semicolon use. Start by specifying the @typescript-eslint/indent rule with the value ["error", 2]. Make sure to include @typescript-eslint in the rule name; otherwise, the configuration will target ESLint’s base rule indent and not the typescript-eslint’s rule indent.

{
  "rules": {
    "no-console": "warn",
+   "@typescript-eslint/indent": ["error", 2]
  }
}

The value for this rule is different to that of no-console. It is an array of parameters. The first value refers to the lint level. It can have the value of "error" (default), "warn", or "off". Awfully familiar right? The second value is the number of spaces for space-based indentation or the string "tab" for tab based indentation. The third and optional value is an object of language specific configurations. Visit the @typescript-eslint/eslint-plugin indent rule documentation for more details.

Based on that documentation, Node.js standard development uses 2-space indentation. Configure the rule and run npm run lint to see the indentation errors in the learn-typescript-linting project. Run the command with the fix option npm run lint -- --fix to automatically fix the errors.

Configuring semicolons requires specifying the semi rule. This rule is similar to the indent rule as it is best configured using an array of parameters. The first parameter is one of the three values "error", "warn", or "off" (default). The second parameter is a string of either "always" or "never". By default, ESLint does not care if semicolons are used or not as JavaScript automatically inserts semicolons. This rule can also be specified with a singular value of "error", "warn", or "off" (default). The configurations and resulting behaviour is best described in a table:

configuration code result
"error" or ["error", "always"] x = 0
x = 0;
error ❌ Missing semicolon
pass ✅
["error", "never"] x = 0
x = 0;
pass ✅
error ❌ Extra semicolon
"warn" or ["warn", "always"] x = 0
x = 0;
warn ⚠️ Missing semicolon
pass ✅
["warn", "never"] x = 0
x = 0;
pass ✅
warn ⚠️ Extra semicolon
"off" or ["off", "always"] or ["off", "never"] x = 0
x = 0;
pass ✅

My preference is sans semicolons, so I’ll be modifying my .eslintrc.json file rule set like so:

{
  "rules": {
    "no-console": "warn",
    "@typescript-eslint/indent": ["error", 2],
+   "semi": ["error", "never"]
  }
}

2.7 Fixing unused variable definition error from type import

[toc]

This final section covers a common problem case when linting TypeScript with ESLint. If you do not specify plugin:@typescript-eslint/recommended in the .eslintrc.json configuration "extends" list, then a troubling error will be returned from the learn-typescript-linting project. Modify the code by removing the second value in the "extends" list, or checkout the learn-typescript-linting/unused-variable branch to see the error.

/learn-typescript-linting/src/bar.ts
  1:10  error  'CustomType' is defined but never used  no-unused-vars

/learn-typescript-linting/src/index.ts
   1:10  error    'CustomType' is defined but never used  no-unused-vars
   9:1   warning  Unexpected console statement            no-console
  10:1   warning  Unexpected console statement            no-console

✖ 4 problems (2 errors, 2 warnings)

ESLint thinks that CustomType is never used; however, the source code is definitely using it, just not in the way ESLint expects it to. This error happens because of how ESLint works. ESLint parses JavaScript into a data structure called an Abstract Syntax Tree (AST) and analyzes the AST using the configured rule set. An AST is a pragmatic data structure that allows a script to interact and perform functions with other, existing code without having to run the code. In this situation, the ESLint parsers are generating the AST from the TypeScript source code, and the ESLint plugins are the scripts analyzing the parsed source code. ESLint does not natively understand how to parse TypeScript; thus, the use of the typescript-eslint/parser. The linter then utilizes the typescript-eslint/eslint-plugin to enable a rule set to analyze the parsed TypeScrtipt code. For more information on the inner workings of typescript-eslint read their documentation on How does typescript-eslint work and why do you have multiple packages.

To fix the above no-unused-vars error, set two rule configurations.

{
- "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"]
+ "extends": ["eslint:recommended"],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "env": { "node": true },
  "parserOptions": {
    "ecmaVersion": 5,
    "sourceType": "module"
  },
  "rules": {
    "no-console": "warn",
    "@typescript-eslint/indent": ["error", 2],
    "semi": ["error", "never"],
+   "no-unused-vars": "off",
+   "@typescript-eslint/no-unused-vars": ["error"]
  }
}

This configuration turns off the base ESLint rule and enables the typescript-eslint rule instead. The typescript-eslint rule understands how to analyze TypeScript source code and will still catch normal JavaScript based unused variables. The plugin:@typescript-eslint/recommended specification creates this specification automatically which is why the error didn’t appear earlier in this guide. Similar to the previous rules, additional configuration is available for the no-unused-vars rule.

3 Additional Resources and Documentation

[toc]


Thank you for reading! If you enjoyed this article follow @MatterhornDev on Twitter for notifications on all future posts. This article was written by Ethan Arrowood, share you support on Twitter by following him (@ArrowoodTech) and sharing this article.

Special thank you’s to Julia Cotter and Colin Hennessey for their help on reviewing and proof reading this article. Find them on GitHub and LinkedIn below!