Migrating Angular projects from TSLint to ESLint and Prettier

Krishnan Mudaliar
Acute Angular
Published in
10 min readOct 2, 2021

--

TSLint has been deprecated for more than 2 years now, nor does Angular 12+ add lint configuration in angular.json when generating new applications anymore. In this article, let’s see how to migrate Angular projects from TSLint to ESLint and Prettier, and add full IDE support (VS Code) too. We shall also go a little deeper to understand their default configs (set of rules) and how to change and override them.

Not a prerequisite, but, if possible, try to upgrade your projects to the latest version of Angular first, for hassle-free setup of ESLint and Prettier.

README First

I cannot stress this enough: read the official docs thoroughly and take notes on topics applicable to your project. This article will discuss many of the important steps, issues and recommendations, but it may not be exhaustive. Also, the official docs contain inter-connected notes on Angular, ESLint, Prettier and VS Code — you don’t want to skip that.

The golden rule #sarcasm

Here are my recommended list of docs:

  1. Angular ESLint (the first and most important of all)
  2. Angular ESLint Premade Configs (default rules-sets explanation)
  3. TypeScript ESLint Premade Configs (default rules-sets explanation)
  4. Map of old TSLint Rules and new TypeScript ESLint Rules
  5. Prettier install doc
  6. Prettier ESLint Integration

I’ll wait. #goreadme

In the spirit of T-DRY, some of the “steps” explanations below will be short but with links to appropriate docs that cover those topics perfectly well. Thereby, I can focus on the ones that are not so obvious to understand, time-consuming to find, or realized only after the fact.

Note: If you are going to refer to this article as a step-by-step guide for the migration process, then please do not skip any steps: go depth-first; get into the links; execute each step fully, then move to the next.

Migration process / workflow

  1. Set up ESLint in Angular project
  2. Handle un-migrated TSLint rules
  3. Resolve ESLint performance issue
  4. Set up recommended ESLint rules configs
  5. Review, assess and fix ng lint errors
  6. Set up Prettier in Angular project
  7. Integrate Prettier into ESLint
  8. (Bonus) Lint configuration for CI
  9. (Bonus) VS Code Extensions Setup

Pro-Tip: Commit changes at each step or each cohesive set of changes.

1. Set up ESLint in Angular project

Follow the steps in migrating an Angular CLI project from Codelyzer and TSLint. In case you are wondering, here’s why we want to use Angular-ESLint vs. wiring-up ESLint manually.

After the migration, you may notice that code files that previously contained tslint:disable*[:rule-name] code comments have been automatically updated to eslint-disable*[ rule-name]. What is disable*? Read TSLint disable rule and ESLint disable rule.

Pro-Tip: In .eslintrc.json, you can add "reportUnusedDisableDirectives": true to ensure that the migrated comments are still relevant. With this option, ng lint will throw errors for unused, irrelevant or improper eslint-disable comments in code.

2. Handle un-migrated TSLint rules

Pay attention to CLI output of the commands run in Step 1. Angular ESLint migration schematics converts almost every single TSLint rule to an ESLint alternative, but not all. So, if you had explicitly configured a lot of TSLint rules in your project, chances are that some of those rules would be printed in the CLI output as “could not migrate these rules…” (paraphrased). There is nothing much you can do about it other than finding the closest alternative rule or creating a custom ESLint rule yourself.

3. Resolve ESLint performance issue

As per my findings, ESLint will run slow for any Angular-CLI-generated project right after the migration. To give you some perspective, ng lint should ideally finish execution within a few seconds for a 100-files project, and about a minute for a 2000-files project (assumption: 4 files per component — html, css, spec, ts). If your execution time looks fine, you may skip this step. Otherwise, please continue.

Reason: Read angular-eslint performance notes. It talks about how a TypeScript Program is created under the hood, why new Programs might be getting created per TS file, how to debug and verify it, and how ESLint config’s parserOptions.project: ["tsconfig.app.json"] matters in all this.

The default tsconfig.app.json configuration might look like this:

{
"extends": "./tsconfig.json",
"compilerOptions": {...},
"files": ["src/main.ts", "src/polyfills.ts"],
"include": ["src/**/*.d.ts"]
}

I was under the impression that TypeScript starts at main.ts, parses its imports and loads other TS files as dependencies. That is how ng build works. But, I don’t know why ng lint doesn’t work that way. Moving on.

Solution: As per the doc, the [worst-case] solution is to create another tsconfig specifically to run ESLint, like tsconfig.eslint.json, and set this file in parserOptions.project. That is what we will do here.

The final tsconfig.eslint.json configuration should look like this:

{
"extends": "./tsconfig.json",
"compilerOptions": { /* copy from tsconfig.app.json */ },
"files": ["src/main.ts", "src/polyfills.ts"],
"include": ["src/**/*.ts"]
}

Explanation: Setting "include": ["src/**/*.ts"] tells TypeScript to treat ALL TS files as part of the same TypeScript Program. So, if you run ng lint in debug mode now, you will see that only one or two Programs get created, instead of one-per-file earlier.

Side Note: I had tried changing tsconfig.app.json to include *.ts instead of *.d.ts. That worked for ng lint, but ng build started failing; Angular compiler didn’t like that. Hence, had to take the worst-case-solution route.

4. Set up recommended ESLint rules configs

Open .eslintrc.json > "extended" property. Its array value may include ng-cli-compat.json and ng-cli-compat--formatting-add-on.json. Read this doc to know why that is so. The doc also mentions that “we will encourage people more and more to move towards the recommended config instead, because this will not be static, it will evolve as recommendations from the Angular Team and community do”. This is the reason why we want to perform this step. But, before we proceed, let us understand where these recommended rules are defined and how they work.

ESLint Premade Configs

It is important to realize that there are 3 layers of Linters here. And, each of these layers come with their own set of “premade configs” based on coding best practices and standards.

  1. eslint: This is the core linter, also containing lots of plugin-rules for linting JavaScript/ECMAScript code.
    Ref: premade config
  2. typescript-eslint: This is written on top of eslint, leveraging typing information made available by the TypeScript Compiler.
    Ref: premade configs
  3. angular-eslint: This is written on top of typescript-eslint and provides further capabilities like parsers for analyzing Angular Templates, and also defines Angular-specific rules, e.g. use-component-selector.
    Ref: premade configs
  4. Third-party plugins: This is not a layer, but independent packages that define specific lint rules, e.g. prefer-arrow-functions.

The references above will come in handy when exploring the exact rules that are turned on, or adding new ones not covered in the recommended configs, or deleting redundant rules defined in your overrides config.

Importance of Hierarchy of Rules

Reasonably, rules from one package may overlap or conflict with rules in another package. Hence, it is very important to extend or override the rules in the right order. For e.g., in the "extends" configuration, if you listed eslint-recommended below plugin:@typescript-eslint/recommended, some rules that were turned OFF by typescript-eslint-recommended may be turned back on by eslint-recommended, and hence throw linting errors incorrectly. Same goes for the ordering of rules in "overrides".

Set up recommended configs

After some studying of all the premade configs, I found the following order of configs best-suited for my projects.

"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/recommended--extra",
"plugin:@angular-eslint/template/process-inline-templates"
],

You should go for what suits you best. Some additional notes:

  • I did not extend plugin:@typescript-eslint/recommended-requiring-type-checking.
    Reason: My projects are a bit old (and 1800 files big). I knew there would be too many lint errors to solve at this point. So… Later!
  • I did not add plugin:@typescript-eslint/eslint-recommended.
    Reason: plugin:@typescript-eslint/recommended extends it already.
  • I added eslint config first, then typescript-eslint one and finally angular-eslint ones to maintain the hierarchy I described earlier.

Reference: ESLint Rule Definitions

  1. TSLint to ESLint rules map here. This is useful for identifying ESLint rules that have different names from their former TSLint counterparts.
  2. Individual TypeScript-ESLint rule descriptions here.
  3. Individual ESLint rule descriptions here.
  4. Prettier rules here. (Will be useful a little later.)

5. Review, assess and fix ng lint errors

This is totally up to you how you want to do it. I would suggest:

  • Run ng lint (without --fix flag).
  • Copy the entire output from CLI to a text editor, and search or script the heck out to analyze the type of errors.
  • Assess whether to solve them, disable them permanently, or disable them per error case-by-case.
  • Run ng lint --fix this time.
  • Fix the rest manually.

Pro-Tip: Fix one lint rule at a time.

Done. ESLint setup should be finished for your Angular CLI project!

6. Set up Prettier in Angular project

  1. Follow the Install doc to install Prettier in your project via npm. You might be tempted to install VS Code’s Prettier Extension directly, but let’s not do that just yet.
  2. Based on Prettier configuration file support, .prettierrc.json would be the best choice of file name.
  3. Go through Prettier options to override some of the defaults as per your project’s standards.

Like in ESLint, it might take a few iterations of experimenting to come up with a final, stable configuration. Following is the one that worked for me.

{
"printWidth": 100,
"tabWidth": 4,
"endOfLine": "auto", // Working on Windows OS & SVN required this
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "all", // I like it this way, many might not
"arrowParens": "avoid", // Cuz it is consistent with C# Linq
"overrides": [
{
"files": ["*.md", "*.json", "*.conf.js"],
"options": {
"tabWidth": 2
}
}
]
}

7. Integrate Prettier into ESLint

Prettier basic setup

Follow ESLint Prettier Configuration. Once done, ng lint will start catching formatting errors too. It is very simple and straightforward. At the end of it, you should have plugin:prettier/recommended added in your extends array.

You may copy some of the Prettier rules (as configured in the previous section) into .eslintrc.json too. By doing so, Prettier rule violations will show up as errors in your Editor Window directly (after you also install ESLint VS Code Extension — next section). Below is an example.

If you do not like this, feel free to skip this step. Otherwise, make sure the prettier rules are added as shown below. Note that the rule names here are exactly the same as those defined in .prettierrc.json. You may choose to add only a sub-set of these rules, the ones that you want to be displayed as errors in the Editor, for e.g. I did not add printWidth in .eslintrc.json.

Prettier configuration for Angular inline-template components

Prettier + ESLint will mess up your Angular Components with inline-templates (here is why). In a nutshell, we need to use two different overrides for HTML: one which applies @angular-eslint/template rules and, the other, prettier rules. This is how .eslintrc.json may look like for you.

{
"files": ["*.html"],
"extends": ["plugin:@angular-eslint/template/recommended"],
"rules": {
"@angular-eslint/template/conditional-complexity": [
"error", { "maxComplexity": 4 }],
"@angular-eslint/template/cyclomatic-complexity": [
"error", { "maxComplexity": 5 }],
"@angular-eslint/template/no-duplicate-attributes": "error",
"@angular-eslint/template/use-track-by-function": "error"
}
},
{
"files": ["*.html"],
"excludedFiles": ["*inline-template-*.component.html"],
"extends": ["plugin:prettier/recommended"],
"rules": {
"prettier/prettier": ["error", { "parser": "angular" }]
}
}

Prettier: Lint. Fix. Repeat.

You are almost done. At this point, you have to buckle-up and start fixing errors. (Also remember the “pro-tip”: keep committing your changes.)

npx prettier --check . runs Prettier rules but does not fix them. The ‘.’ at the end tells prettier to run it on the entire project. You may choose to do it for a specific directory or file in the project too. Once you find the configuration to be stable enough, you may run Prettier rules auto-fixes for the entire workspace using npx prettier --write . command.

8. (Bonus) Lint configuration for CI

To make sure that your CI too catches ESLint and Prettier formatting errors, in case developers didn’t run these hygiene checks themselves, you may add the following to your project’s npm scripts.

"scripts": {
...
"lint": "prettier --check . && ng lint",
...
},

Then, all you need to do is add npm run lint as one of your CI steps. Of course, instead of npm scripts, you could directly add npx prettier --check . and ng lint commands directly as CI steps too.

9. (Bonus) VS Code Extensions Setup

The setup done till now will work perfectly well when you run lint from CLI. However, if you want these errors to appear at real-time in VS Code while you edit your project files too, the following extensions will provide you the best developer experience.

Ensuring consistent development experience with VS Code

  1. I would encourage you to create your own Extensions Pack, wherein you can add all the extensions you want your team to have when they open the project in VS Code. It is pretty easy. And there are lots of articles out there on how to do it.
  2. Each Extension may come with its own set of settings. If you change those settings according to your projects’ needs, you might want to have them applied for everyone in your team too. If so, make sure you change Workspace Settings instead of User Settings. This will create a file .vscode/settings.json at the root of your project’s folder. Commit this file into your repo.

For example, here is the Workspace Settings configured for my Angular project.

{
"editor.detectIndentation": false,
"eslint.options": {
"extensions": [".ts", ".html"]
},
"eslint.probe": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"html",
"markdown"
],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true
}

And we are done.

Appendix — Other references

--

--

Krishnan Mudaliar
Acute Angular

Loves the web • DIY • DRY • ASP.NET • JavaScript • Angular • NodeJS (i/bitsy) • Believer of great UX