TSLint was deprecated by its creators all the way back in 2019: palantir/tslint#4534
The Angular CLI stopped supporting their TSLint builder implementation (to power ng lint
) as of version 13, which is now 3 (or maybe more depending on when you are reading this) major versions ago, meaning it is at least 1.5 years ago.
During the initial couple of years angular-eslint was delighted to be able to provide valuable tooling to help with a mostly automated transition from TSLint to ESLint for Angular CLI projects.
As a community project we need to focus on what adds the most value to the majority of our users, and so, in version 16, this conversion tooling was removed.
If you want to leverage it, you can of course use it on a major version of Angular + angular-eslint prior to version 16, or you can feel free to take the implementation from our git history and leverage the schematics inline in your own projects.
If you are looking for general help in migrating specific rules from TSLint to ESLint, you can check out this incredible project that we depended on in our conversion schematic: https://github.com/typescript-eslint/tslint-to-eslint-config
Our original guide and Codelyzer equivalence table, both of which correspond to angular-eslint version 15, can be found below, in addition to the legacy ng-cli-compat
and ng-cli-compat-formatting-add-on
configs.
We have some tooling to make this as automated as possible, but the reality is it will always be somewhat project-specific as to how much work will be involved in the migration.
The first step is to run the schematic to add @angular-eslint
to your project:
ng add @angular-eslint/schematics
This will handle installing the latest version of all the relevant packages for you and adding them to the devDependencies
of your package.json
.
If you just have a single project in your workspace you can just run:
ng g @angular-eslint/schematics:convert-tslint-to-eslint
If you have a projects/
directory or similar in your workspace, you will have multiple entries in your projects
configuration and you will need to chose which one you want to migrate using the convert-tslint-to-eslint
schematic:
ng g @angular-eslint/schematics:convert-tslint-to-eslint {{YOUR_PROJECT_NAME_GOES_HERE}}
The schematic will do the following for you:
- Read your chosen project's
tslint.json
and use it to CREATE a.eslintrc.json
at the root of the specific project which extends from the root config (if you do not already have a root config, it will also add one automatically for you).- The contents of this
.eslintrc.json
will be the closest possible equivalent to yourtslint.json
that the tooling can figure out. - You will want to pay close attention to the terminal output of the schematic as it runs, because it will let you know if it couldn't find an appropriate converter for a TSLint rule, or if it has installed any additional ESLint plugins to help you match up your new setup with your old one.
- The contents of this
- UPDATE the project's
architect
configuration in theangular.json
to such that thelint
"target" will invoke ESLint instead of TSLint. - UPDATE any instances of
tslint:disable
comments that are located within your TypeScript source files to their ESLint equivalent. - If you choose YES (the default) for the
--remove-tslint-if-no-more-tslint-targets
option, it will also automatically remove TSLint and Codelyzer from your workspace if you have no more usage of them left.
Now when you run:
npx ng lint {{YOUR_PROJECT_NAME_GOES_HERE}}
...you are running ESLint on your project! 🎉
The table below shows the status of each Codelyzer Rule in terms of whether or not an equivalent for it has been created within @angular-eslint
.
If you see a rule below that has no status against it, then please feel free to open a PR with an appropriate implementation. You can look at the Codelyzer repo and the existing plugins within this repo for inspiration.
Explanation of Statuses |
---|
✅ = We have created an ESLint equivalent of this TSLint rule |
🚧 = There is an open PR to provide an ESLint equivalent of this TSLint rule |
🙅 = This TSLint rule has been replaced by functionality within the Angular compiler, or should be replaced by a dedicated code formatter, such as Prettier |
Codelyzer Rule | ESLint Equivalent | Status |
---|---|---|
contextual-decorator |
@angular-eslint/contextual-decorator | ✅ |
contextual-lifecycle |
@angular-eslint/contextual-lifecycle | ✅ |
no-attribute-decorator |
@angular-eslint/no-attribute-decorator | ✅ |
no-lifecycle-call |
@angular-eslint/no-lifecycle-call | ✅ |
no-output-native |
@angular-eslint/no-output-native | ✅ |
no-pipe-impure |
@angular-eslint/no-pipe-impure | ✅ |
prefer-on-push-component-change-detection |
@angular-eslint/prefer-on-push-component-change-detection | ✅ |
template-accessibility-alt-text |
@angular-eslint/template/accessibility-alt-text | ✅ |
template-accessibility-elements-content |
@angular-eslint/template/accessibility-elements-content | ✅ |
template-accessibility-label-for |
@angular-eslint/template/accessibility-label-for | ✅ |
template-accessibility-tabindex-no-positive |
@angular-eslint/template/no-positive-tabindex | ✅ |
template-accessibility-table-scope |
@angular-eslint/template/accessibility-table-scope | ✅ |
template-accessibility-valid-aria |
@angular-eslint/template/accessibility-valid-aria | ✅ |
template-banana-in-box |
@angular-eslint/template/banana-in-box | ✅ |
template-click-events-have-key-events |
@angular-eslint/template/click-events-have-key-events | ✅ |
template-mouse-events-have-key-events |
@angular-eslint/template/mouse-events-have-key-events | ✅ |
template-no-any |
@angular-eslint/template/no-any | ✅ |
template-no-autofocus |
@angular-eslint/template/no-autofocus | ✅ |
template-no-distracting-elements |
@angular-eslint/template/no-distracting-elements | ✅ |
template-no-negated-async |
@angular-eslint/template/no-negated-async | ✅ |
use-injectable-provided-in |
@angular-eslint/use-injectable-provided-in | ✅ |
use-lifecycle-interface |
@angular-eslint/use-lifecycle-interface | ✅ |
Codelyzer Rule | ESLint Equivalent | Status |
---|---|---|
component-max-inline-declarations |
@angular-eslint/component-max-inline-declarations | ✅ |
no-conflicting-lifecycle |
@angular-eslint/no-conflicting-lifecycle | ✅ |
no-forward-ref |
@angular-eslint/no-forward-ref | ✅ |
no-input-prefix |
@angular-eslint/no-input-prefix | ✅ |
no-input-rename |
@angular-eslint/no-input-rename | ✅ |
no-output-on-prefix |
@angular-eslint/no-output-on-prefix | ✅ |
no-output-rename |
@angular-eslint/no-output-rename | ✅ |
no-unused-css |
||
prefer-output-readonly |
@angular-eslint/prefer-output-readonly | ✅ |
relative-url-prefix |
@angular-eslint/relative-url-prefix | ✅ |
template-conditional-complexity |
@angular-eslint/template/conditional-complexity | ✅ |
template-cyclomatic-complexity |
@angular-eslint/template/cyclomatic-complexity | ✅ |
template-i18n |
@angular-eslint/template/i18n | ✅ |
template-no-call-expression |
@angular-eslint/template/no-call-expression | ✅ |
template-use-track-by-function |
@angular-eslint/template/use-track-by-function | ✅ |
use-component-selector |
@angular-eslint/use-component-selector | ✅ |
use-component-view-encapsulation |
@angular-eslint/use-component-view-encapsulation | ✅ |
use-pipe-decorator |
N/A, see explanation above | 🙅 |
use-pipe-transform-interface |
@angular-eslint/use-pipe-transform-interface | ✅ |
Codelyzer Rule | ESLint Equivalent | Status |
---|---|---|
angular-whitespace |
N/A, see explanation above | 🙅 |
component-class-suffix |
@angular-eslint/component-class-suffix | ✅ |
component-selector |
@angular-eslint/component-selector | ✅ |
directive-class-suffix |
@angular-eslint/directive-class-suffix | ✅ |
directive-selector |
@angular-eslint/directive-selector | ✅ |
import-destructuring-spacing |
N/A, see explanation above | 🙅 |
no-host-metadata-property |
@angular-eslint/no-host-metadata-property | ✅ |
no-inputs-metadata-property |
@angular-eslint/no-inputs-metadata-property | ✅ |
no-outputs-metadata-property |
@angular-eslint/no-outputs-metadata-property | ✅ |
no-queries-metadata-property |
@angular-eslint/no-queries-metadata-property | ✅ |
pipe-prefix |
@angular-eslint/pipe-prefix | ✅ |
prefer-inline-decorator |
N/A, see explanation above | 🙅 |
We strongly encourage migrating to extend from the recommended configs from both
typescript-eslint
andangular-eslint
as soon as possible.
If you ever used the convert-tslint-to-eslint
schematic in the past, you might have noticed that it generated a config which extended from ng-cli-compat
and ng-cli-compat--formatting-add-on
.
As you might infer from the names, these configs existed to most closely match what the Angular CLI used to configure for TSLint and help us reduce a lot of the boilerplate config as part of the TSLint -> ESLint conversion.
You are free to remove them or customize them in any way you wish. Over time, 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.
Note: The equivalent TSLint config from the Angular CLI = both ng-cli-compat
+ ng-cli-compat--formatting-add-on
.
The reason for separating out the formatting related rules was that we fundamentally believe you should not use a linter for formatting concerns (you should use a dedicated code formatting tool like Prettier instead), and having them in a separate config that is extended from makes it super easy to remove if you choose to.
We strongly encourage migrating to extend from the recommended configs from both typescript-eslint
and angular-eslint
as soon as possible. These configs are kept up to date as recommendations across the various ecosystems evolve.
If you would like to recreate the ng-cli-compat
and ng-cli-compat--formatting-add-on
configs as they exists in v15 and earlier, then you can use the following:
ng-cli-compat
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "@angular-eslint"],
"env": {
"browser": true,
"es6": true,
"node": true
},
"plugins": [
"eslint-plugin-import",
"eslint-plugin-jsdoc",
"eslint-plugin-prefer-arrow"
],
"rules": {
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"sort-keys": "off",
"@angular-eslint/component-class-suffix": "error",
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"@angular-eslint/contextual-lifecycle": "error",
"@angular-eslint/directive-class-suffix": "error",
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/no-conflicting-lifecycle": "error",
"@angular-eslint/no-host-metadata-property": "error",
"@angular-eslint/no-input-rename": "error",
"@angular-eslint/no-inputs-metadata-property": "error",
"@angular-eslint/no-output-native": "error",
"@angular-eslint/no-output-on-prefix": "error",
"@angular-eslint/no-output-rename": "error",
"@angular-eslint/no-outputs-metadata-property": "error",
"@angular-eslint/use-lifecycle-interface": "error",
"@angular-eslint/use-pipe-transform-interface": "error",
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "off",
"@typescript-eslint/ban-types": [
"error",
{
"types": {
"Object": {
"message": "Avoid using the `Object` type. Did you mean `object`?"
},
"Function": {
"message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`."
},
"Boolean": {
"message": "Avoid using the `Boolean` type. Did you mean `boolean`?"
},
"Number": {
"message": "Avoid using the `Number` type. Did you mean `number`?"
},
"String": {
"message": "Avoid using the `String` type. Did you mean `string`?"
},
"Symbol": {
"message": "Avoid using the `Symbol` type. Did you mean `symbol`?"
}
}
}
],
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/dot-notation": "error",
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/naming-convention": "error",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-inferrable-types": [
"error",
{
"ignoreParameters": true
}
],
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/triple-slash-reference": [
"error",
{
"path": "always",
"types": "prefer-import",
"lib": "always"
}
],
"@typescript-eslint/unified-signatures": "error",
"complexity": "off",
"constructor-super": "error",
"eqeqeq": ["error", "smart"],
"guard-for-in": "error",
"id-blacklist": [
"error",
"any",
"Number",
"number",
"String",
"string",
"Boolean",
"boolean",
"Undefined",
"undefined"
],
"id-match": "error",
"import/no-deprecated": "warn",
"jsdoc/newline-after-description": "error",
"jsdoc/no-types": "error",
"max-classes-per-file": "off",
"no-bitwise": "error",
"no-caller": "error",
"no-cond-assign": "error",
"no-console": [
"error",
{
"allow": [
"log",
"warn",
"dir",
"timeLog",
"assert",
"clear",
"count",
"countReset",
"group",
"groupEnd",
"table",
"dirxml",
"error",
"groupCollapsed",
"Console",
"profile",
"profileEnd",
"timeStamp",
"context"
]
}
],
"no-debugger": "error",
"no-empty": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-invalid-this": "off",
"no-new-wrappers": "error",
"no-restricted-imports": [
"error",
{
"name": "rxjs/Rx",
"message": "Please import directly from 'rxjs' instead"
}
],
"@typescript-eslint/no-shadow": [
"error",
{
"hoist": "all"
}
],
"no-throw-literal": "error",
"no-undef-init": "error",
"no-underscore-dangle": "error",
"no-unsafe-finally": "error",
"no-unused-labels": "error",
"no-var": "error",
"object-shorthand": "error",
"one-var": ["error", "never"],
"prefer-arrow/prefer-arrow-functions": "error",
"prefer-const": "error",
"radix": "error",
"use-isnan": "error",
"valid-typeof": "off"
}
}
ng-cli-compat--formatting-add-on
{
"plugins": ["eslint-plugin-jsdoc"],
"rules": {
"arrow-body-style": "error",
"arrow-parens": "off",
"comma-dangle": "off",
"curly": "error",
"eol-last": "error",
"jsdoc/check-alignment": "error",
"max-len": [
"error",
{
"code": 140
}
],
"new-parens": "error",
"no-multiple-empty-lines": "off",
"no-trailing-spaces": "error",
"quote-props": ["error", "as-needed"],
"space-before-function-paren": [
"error",
{
"anonymous": "never",
"asyncArrow": "always",
"named": "never"
}
],
"@typescript-eslint/member-delimiter-style": [
"error",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
"quotes": "off",
"@typescript-eslint/quotes": [
"error",
"single",
{ "allowTemplateLiterals": true }
],
"@typescript-eslint/semi": ["error", "always"],
"@typescript-eslint/type-annotation-spacing": "error"
}
}