Skip to main content

Announcing typescript-eslint v8

· 17 min read
Josh Goldberg
typescript-eslint Maintainer

typescript-eslint is the tooling that enables standard JavaScript tools such as ESLint and Prettier to support TypeScript code. We've been working on a set of breaking changes and general features that we're excited to get in front of users. And now, we're excited to say that typescript-eslint v8 is released as stable! 🎉

We'd previously blogged about v8 in Announcing typescript-eslint v8 Beta. This blog post contains much of the same information as that one.

Trying Out v8

Whether you're new to linting your TypeScript code or a returning user, please do upgrade to the latest major version of typescript-eslint! V8 comes with a suite of quality-of-life improvements we think you'll appreciate.

As A New User

If you don't yet use typescript-eslint, you can go through our configuration steps on the v8 Getting Started docs. It'll walk you through setting up typescript-eslint in a project.

As An Existing User

If you already use typescript-eslint, you'll need to first replace your package's previous versions with @8:

npm i typescript-eslint@8 --save-dev

We highly recommend then basing your ESLint configuration on the reworked typescript-eslint recommended configurations — especially if it's been a while since you've reworked your linter config.

User-Facing Changes

These are the changes that users of typescript-eslint —generally, any developer running ESLint on TypeScript code— should pay attention to when upgrading typescript-eslint from v7 to v8.

ESLint v9 Support

typescript-eslint v8 ships with full support for ESLint v9.

typescript-eslint v7 was our first version that supported ESLint's new "flat" config file format, which was already available in ESLint v8. ESLint v9 still supports ESLint's older legacy config file format so our tooling does as well. However, ESLint v9 also includes a set of breaking changes that we added support for in typescript-eslint v8. See the ESLint v9 release blog post for more details.

Project Service

The biggest new feature added in this version is the stability of our new "project service". In short, the project service is a new way to enable typed linting that is generally easier to configure and faster at runtime than our previous offerings. It's been experimentally available since v6.1.0 under the name EXPERIMENTAL_useProjectService; now, we've renamed it to projectService.

You can use the new project service in your configuration instead of the previous parserOptions.project:

eslint.config.mjs
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
project: true,
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
);

The project service will automatically find the closest tsconfig.json for each file (like project: true). It also allows enabling typed linting for files not explicitly included in a tsconfig.json. This should remove the need for custom tsconfig.eslint.json files to lint files like eslint.config.mjs!

Typed linting for out-of-project files can be done by specifying two properties of a parserOptions.projectService object:

  • allowDefaultProject: a glob of a small number of out-of-project files to enable a slower default project on
  • defaultProject: path to a TypeScript configuration file to use for the slower default project
eslint.config.mjs
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
project: ['packages/*/tsconfig.json', 'tsconfig.eslint.json'],
projectService: {
allowDefaultProject: ['./*.js'],
defaultProject: './tsconfig.json',
},
tsconfigRootDir: import.meta.dirname,
},
},
},
);

Internally, the project service uses the same TypeScript APIs that editors such as VS Code use. Doing so should make it harder to accidentally configure different type information for ESLint than what you see in day-to-day editing.

We're thrilled to have the project service option promoted to stable in v8. We'll soon release a dedicated parserOptions blog post walking through the new option in more details. 🚀

Updated Configuration Rules

Every new major version of typescript-eslint comes with changes to which rules are enabled in the preset configurations and with which options. See the table in Changes to configurations for 8.0.0 for more context on the changes.

Please do try out the new rule configurations presets and let us know in that discussion!

tip

If your ESLint configuration contains many rules configurations, we suggest the following strategy to start anew:

  1. Remove all your rules configurations
  2. Extend from the preset configs that make sense for you
  3. Run ESLint on your project
  4. In your ESLint configuration, turn off any rules creating errors that don't make sense for your project — with comments explaining why
  5. In your ESLint configuration and/or with inline eslint-disable comments, turn off / downgrade to "warn" any rules creating too many errors for you to fix — with "TODO" comments linking to tracking issues/tickets to re-enable them
Diff patch from v7 to v8 for recommended
{
'@typescript-eslint/ban-ts-comment': '...',
- '@typescript-eslint/ban-types': '...',
'no-array-constructor': '...',
'@typescript-eslint/no-array-constructor': '...',
'@typescript-eslint/no-duplicate-enum-values': '...',
+ '@typescript-eslint/no-empty-object-type': '...',
'@typescript-eslint/no-explicit-any': '...',
'@typescript-eslint/no-extra-non-null-assertion': '...',
- 'no-loss-of-precision': '...',
- '@typescript-eslint/no-loss-of-precision': '...',
'@typescript-eslint/no-misused-new': '...',
'@typescript-eslint/no-namespace': '...',
'@typescript-eslint/no-non-null-asserted-optional-chain': '...',
+ '@typescript-eslint/no-require-imports': '...',
'@typescript-eslint/no-this-alias': '...',
'@typescript-eslint/no-unnecessary-type-constraint': '...',
'@typescript-eslint/no-unsafe-declaration-merging': '...',
+ '@typescript-eslint/no-unsafe-function-type': '...',
+ 'no-unused-expressions': '...',
+ '@typescript-eslint/no-unused-expressions': '...',
'no-unused-vars': '...',
'@typescript-eslint/no-unused-vars': '...',
- '@typescript-eslint/no-var-requires': '...',
+ '@typescript-eslint/no-wrapper-object-types': '...',
'@typescript-eslint/prefer-as-const': '...',
+ '@typescript-eslint/prefer-namespace-keyword': '...',
'@typescript-eslint/triple-slash-reference': '...',
}
Diff patch from v7 to v8 for recommended-type-checked
{
'@typescript-eslint/await-thenable': '...',
'@typescript-eslint/ban-ts-comment': '...',
- '@typescript-eslint/ban-types': '...',
'no-array-constructor': '...',
'@typescript-eslint/no-array-constructor': '...',
+ '@typescript-eslint/no-array-delete': '...',
'@typescript-eslint/no-base-to-string': '...',
'@typescript-eslint/no-duplicate-enum-values': '...',
'@typescript-eslint/no-duplicate-type-constituents': '...',
+ '@typescript-eslint/no-empty-object-type': '...',
'@typescript-eslint/no-explicit-any': '...',
'@typescript-eslint/no-extra-non-null-assertion': '...',
'@typescript-eslint/no-floating-promises': '...',
'@typescript-eslint/no-for-in-array': '...',
'no-implied-eval': '...',
'@typescript-eslint/no-implied-eval': '...',
- 'no-loss-of-precision': '...',
- '@typescript-eslint/no-loss-of-precision': '...',
'@typescript-eslint/no-misused-new': '...',
'@typescript-eslint/no-misused-promises': '...',
'@typescript-eslint/no-namespace': '...',
'@typescript-eslint/no-non-null-asserted-optional-chain': '...',
'@typescript-eslint/no-redundant-type-constituents': '...',
+ '@typescript-eslint/no-require-imports': '...',
'@typescript-eslint/no-this-alias': '...',
+ 'no-throw-literal': '...',
'@typescript-eslint/no-unnecessary-type-assertion': '...',
'@typescript-eslint/no-unnecessary-type-constraint': '...',
'@typescript-eslint/no-unsafe-argument': '...',
'@typescript-eslint/no-unsafe-assignment': '...',
'@typescript-eslint/no-unsafe-call': '...',
'@typescript-eslint/no-unsafe-declaration-merging': '...',
'@typescript-eslint/no-unsafe-enum-comparison': '...',
+ '@typescript-eslint/no-unsafe-function-type': '...',
'@typescript-eslint/no-unsafe-member-access': '...',
'@typescript-eslint/no-unsafe-return': '...',
+ '@typescript-eslint/no-unsafe-unary-minus': '...',
+ 'no-unused-expressions': '...',
+ '@typescript-eslint/no-unused-expressions': '...',
'no-unused-vars': '...',
'@typescript-eslint/no-unused-vars': '...',
- '@typescript-eslint/no-var-requires': '...',
+ '@typescript-eslint/no-wrapper-object-types': '...',
+ '@typescript-eslint/only-throw-error': '...',
'@typescript-eslint/prefer-as-const': '...',
+ '@typescript-eslint/prefer-namespace-keyword': '...',
+ 'prefer-promise-reject-errors': '...',
+ '@typescript-eslint/prefer-promise-reject-errors': '...',
'require-await': '...',
'@typescript-eslint/require-await': '...',
'@typescript-eslint/restrict-plus-operands': '...',
'@typescript-eslint/restrict-template-expressions': '...',
'@typescript-eslint/triple-slash-reference': '...',
'@typescript-eslint/unbound-method': '...',
}
Diff patch from v7 to v8 for strict
{
'@typescript-eslint/ban-ts-comment': '...',
- '@typescript-eslint/ban-types': '...',
'no-array-constructor': '...',
'@typescript-eslint/no-array-constructor': '...',
'@typescript-eslint/no-duplicate-enum-values': '...',
'@typescript-eslint/no-dynamic-delete': '...',
+ '@typescript-eslint/no-empty-object-type': '...',
'@typescript-eslint/no-explicit-any': '...',
'@typescript-eslint/no-extra-non-null-assertion': '...',
'@typescript-eslint/no-extraneous-class': '...',
'@typescript-eslint/no-invalid-void-type': '...',
- 'no-loss-of-precision': '...',
- '@typescript-eslint/no-loss-of-precision': '...',
'@typescript-eslint/no-misused-new': '...',
'@typescript-eslint/no-namespace': '...',
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': '...',
'@typescript-eslint/no-non-null-asserted-optional-chain': '...',
'@typescript-eslint/no-non-null-assertion': '...',
+ '@typescript-eslint/no-require-imports': '...',
'@typescript-eslint/no-this-alias': '...',
'@typescript-eslint/no-unnecessary-type-constraint': '...',
'@typescript-eslint/no-unsafe-declaration-merging': '...',
+ '@typescript-eslint/no-unsafe-function-type': '...',
+ 'no-unused-expressions': '...',
+ '@typescript-eslint/no-unused-expressions': '...',
'no-unused-vars': '...',
'@typescript-eslint/no-unused-vars': '...',
'no-useless-constructor': '...',
'@typescript-eslint/no-useless-constructor': '...',
- '@typescript-eslint/no-var-requires': '...',
+ '@typescript-eslint/no-wrapper-object-types': '...',
'@typescript-eslint/prefer-as-const': '...',
'@typescript-eslint/prefer-literal-enum-member': '...',
+ '@typescript-eslint/prefer-namespace-keyword': '...',
'@typescript-eslint/triple-slash-reference': '...',
'@typescript-eslint/unified-signatures': '...',
}
Diff patch from v7 to v8 for strict-type-checked
{
'@typescript-eslint/await-thenable': '...',
'@typescript-eslint/ban-ts-comment': '...',
- '@typescript-eslint/ban-types': '...',
'no-array-constructor': '...',
'@typescript-eslint/no-array-constructor': '...',
'@typescript-eslint/no-array-delete': '...',
'@typescript-eslint/no-base-to-string': '...',
'@typescript-eslint/no-confusing-void-expression': '...',
'@typescript-eslint/no-duplicate-enum-values': '...',
'@typescript-eslint/no-duplicate-type-constituents': '...',
'@typescript-eslint/no-dynamic-delete': '...',
+ '@typescript-eslint/no-empty-object-type': '...',
'@typescript-eslint/no-explicit-any': '...',
'@typescript-eslint/no-extra-non-null-assertion': '...',
'@typescript-eslint/no-extraneous-class': '...',
'@typescript-eslint/no-floating-promises': '...',
'@typescript-eslint/no-for-in-array': '...',
'no-implied-eval': '...',
'@typescript-eslint/no-implied-eval': '...',
'@typescript-eslint/no-invalid-void-type': '...',
- 'no-loss-of-precision': '...',
- '@typescript-eslint/no-loss-of-precision': '...',
'@typescript-eslint/no-meaningless-void-operator': '...',
'@typescript-eslint/no-misused-new': '...',
'@typescript-eslint/no-misused-promises': '...',
'@typescript-eslint/no-mixed-enums': '...',
'@typescript-eslint/no-namespace': '...',
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': '...',
'@typescript-eslint/no-non-null-asserted-optional-chain': '...',
'@typescript-eslint/no-non-null-assertion': '...',
'@typescript-eslint/no-redundant-type-constituents': '...',
+ '@typescript-eslint/no-require-imports': '...',
+ 'no-return-await': '...',
'@typescript-eslint/no-this-alias': '...',
'no-throw-literal': '...',
'@typescript-eslint/no-unnecessary-boolean-literal-compare': '...',
'@typescript-eslint/no-unnecessary-condition': '...',
'@typescript-eslint/no-unnecessary-template-expression': '...',
'@typescript-eslint/no-unnecessary-type-arguments': '...',
'@typescript-eslint/no-unnecessary-type-assertion': '...',
'@typescript-eslint/no-unnecessary-type-constraint': '...',
'@typescript-eslint/no-unsafe-argument': '...',
'@typescript-eslint/no-unsafe-assignment': '...',
'@typescript-eslint/no-unsafe-call': '...',
'@typescript-eslint/no-unsafe-declaration-merging': '...',
'@typescript-eslint/no-unsafe-enum-comparison': '...',
+ '@typescript-eslint/no-unsafe-function-type': '...',
'@typescript-eslint/no-unsafe-member-access': '...',
'@typescript-eslint/no-unsafe-return': '...',
+ '@typescript-eslint/no-unsafe-unary-minus': '...',
+ 'no-unused-expressions': '...',
+ '@typescript-eslint/no-unused-expressions': '...',
'no-unused-vars': '...',
'@typescript-eslint/no-unused-vars': '...',
'no-useless-constructor': '...',
'@typescript-eslint/no-useless-constructor': '...',
- '@typescript-eslint/no-var-requires': '...',
+ '@typescript-eslint/no-wrapper-object-types': '...',
'@typescript-eslint/only-throw-error': '...',
'@typescript-eslint/prefer-as-const': '...',
- '@typescript-eslint/prefer-includes': '...',
'@typescript-eslint/prefer-literal-enum-member': '...',
+ '@typescript-eslint/prefer-namespace-keyword': '...',
'prefer-promise-reject-errors': '...',
'@typescript-eslint/prefer-promise-reject-errors': '...',
'@typescript-eslint/prefer-reduce-type-parameter': '...',
'@typescript-eslint/prefer-return-this-type': '...',
'require-await': '...',
'@typescript-eslint/require-await': '...',
'@typescript-eslint/restrict-plus-operands': '...',
'@typescript-eslint/restrict-template-expressions': '...',
+ '@typescript-eslint/return-await': '...',
'@typescript-eslint/triple-slash-reference': '...',
'@typescript-eslint/unbound-method': '...',
'@typescript-eslint/unified-signatures': '...',
'@typescript-eslint/use-unknown-in-catch-callback-variable': '...',
}
Diff patch from v7 to v8 for stylistic
{
'@typescript-eslint/adjacent-overload-signatures': '...',
'@typescript-eslint/array-type': '...',
'@typescript-eslint/ban-tslint-comment': '...',
'@typescript-eslint/class-literal-property-style': '...',
'@typescript-eslint/consistent-generic-constructors': '...',
'@typescript-eslint/consistent-indexed-object-style': '...',
'@typescript-eslint/consistent-type-assertions': '...',
'@typescript-eslint/consistent-type-definitions': '...',
'@typescript-eslint/no-confusing-non-null-assertion': '...',
'no-empty-function': '...',
'@typescript-eslint/no-empty-function': '...',
- '@typescript-eslint/no-empty-interface': '...',
'@typescript-eslint/no-inferrable-types': '...',
'@typescript-eslint/prefer-for-of': '...',
'@typescript-eslint/prefer-function-type': '...',
- '@typescript-eslint/prefer-namespace-keyword': '...',
}
Diff patch from v7 to v8 for stylistic-type-checked
{
'@typescript-eslint/adjacent-overload-signatures': '...',
'@typescript-eslint/array-type': '...',
'@typescript-eslint/ban-tslint-comment': '...',
'@typescript-eslint/class-literal-property-style': '...',
'@typescript-eslint/consistent-generic-constructors': '...',
'@typescript-eslint/consistent-indexed-object-style': '...',
'@typescript-eslint/consistent-type-assertions': '...',
'@typescript-eslint/consistent-type-definitions': '...',
'dot-notation': '...',
'@typescript-eslint/dot-notation': '...',
'@typescript-eslint/no-confusing-non-null-assertion': '...',
'no-empty-function': '...',
'@typescript-eslint/no-empty-function': '...',
- '@typescript-eslint/no-empty-interface': '...',
'@typescript-eslint/no-inferrable-types': '...',
'@typescript-eslint/non-nullable-type-assertion-style': '...',
+ '@typescript-eslint/prefer-find': '...',
'@typescript-eslint/prefer-for-of': '...',
'@typescript-eslint/prefer-function-type': '...',
+ '@typescript-eslint/prefer-includes': '...',
- '@typescript-eslint/prefer-namespace-keyword': '...',
'@typescript-eslint/prefer-nullish-coalescing': '...',
'@typescript-eslint/prefer-optional-chain': '...',
+ '@typescript-eslint/prefer-regexp-exec': '...',
'@typescript-eslint/prefer-string-starts-ends-with': '...',
}

Rule Breaking Changes

Several rules are changed in significant enough ways to be considered breaking changes:

Replacement of ban-types

@typescript-eslint/ban-types has long been one of the more controversial rules in typescript-eslint. It served two purposes:

  • Allowing users to ban a configurable list of types from being used in type annotations
  • Banning confusing or dangerous built-in types such as Function and Number

Notably, ban-types banned the built-in {} ("empty object") type in TypeScript. The {} type is a common source of confusion for TypeScript developers because it matches any non-nullable value, including primitives like "".

Banning {} in ban-types was helpful to prevent developers from accidentally using it instead of a more safe type such as object. On the other hand, there are legitimate uses for {}, and banning it by default was harmful in those cases.

typescript-eslint v8 deletes the ban-types rule and replaces it with several more targeted rules:

To migrate to the new rules:

For more details, see the issues and pull requests that split apart the ban-types rule:

Tooling Breaking Changes

Developer-Facing Changes

typescript-eslint v8 comes with a suite of cleanups and improvements for developers using its Node.js APIs as well. If you author any ESLint plugins or other tools that interact with TypeScript syntax, then we recommend you try out typescript-eslint v8 soon. It includes some breaking changes that you may need to accommodate for.

tip

If you're having trouble working with the changes, please let us know on the typescript-eslint Discord's #v8 channel!

AST Breaking Changes

These changes are to the AST shapes generated by typescript-eslint when parsing code. If you author any ESLint rules that refer to the syntax mentioned by them, these are relevant to you.

Custom Rule meta.docs Types

@typescript-eslint/utils has long exported a RuleCreator utility for making custom well-typed custom ESLint rules. That RuleCreator is used internally by @typescript-eslint/eslint-plugin — and in fact, up through typescript-eslint v7, it hardcoded the same types for rules' meta.docs as @typescript-eslint/eslint-plugin!

In typescript-eslint v8, we've made two changes to RuleCreator:

  • Rule meta.docs by default only allows the properties defined in ESLint's Custom Rules > Rule Structure docs: description and url
  • RuleCreator allows an optional type parameter to specify additional allowed properties

For example, this rule includes the common meta.docs.recommended property as a boolean:

interface MyPluginDocs {
recommended: boolean;
}

const createRule = ESLintUtils.RuleCreator<MyPluginDocs>(
name => `https://example.com/rule/${name}`,
);

createRule({
// ...
meta: {
docs: {
description: '...',
recommended: true,
},
// ...
},
});

See feat(utils): allow specifying additional rule meta.docs in RuleCreator for more details.

Flat Configuration RuleTester

The RuleTester provided by @typescript-eslint/rule-tester is a fork of ESLint's RuleTester. In typescript-eslint v7 and earlier, RuleTester's constructor allowed providing legacy "eslintrc" options -- mirroring ESLint v8 and earlier. In typescript-eslint v8, RuleTester's constructor now instead allows providing new "flat" config options -- mirroring ESLint v9.

Per ESLint flat configs, any parser configurations you provide will need to be inside a languageOptions property:

rule.test.ts
import { RuleTester } from '@typescript-eslint/rule-tester

const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname,
},
},
});

Any parser you provide will need to be the parser itself, rather than a string name of the package:

rule.test.ts
import { RuleTester } from '@typescript-eslint/rule-tester
import jsoncParser from "jsonc-eslint-parser";

const ruleTester = new RuleTester({
languageOptions: {
parser: jsoncParser,
parser: "jsonc-eslint-parser",
},
});

This change brings typescript-eslint's RuleTester in line with ESLint's RuleTester and flat config. In doing so, it changes two parserOptions defaults:

  • ecmaVersion: from 5 to 'latest'
  • sourceType: from 'script' to 'module'

If you were specifying either or both of those in your tests, you likely can now omit them:

rule.test.ts
import { RuleTester } from '@typescript-eslint/rule-tester

const ruleTester = new RuleTester();
const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
});

For more details, see:

Support for multi-pass fixes in RuleTester

One limitation of ESLint's RuleTester is that it is not possible to verify the individual applied fixes when a rule provides multiple rounds of fixes. ESLint's RuleTester applies only the first fix when there is conflict between two fixes.

In typescript-eslint v8, our RuleTester tries to apply all possible fixes for each test case.

If your rule tests had some test cases that required multi-pass fixes, you will see some test failures. To fix these failures, provide an array of strings for output which specifies the output after each fix pass.

import { RuleTester } from '@typescript-eslint/rule-tester';
import rule from '../src/rules/my-rule.ts';

const ruleTester = new RuleTester();

ruleTester.run('my-rule', rule, {
valid: [
/* ... */
],
invalid: [
{
code: 'const a = 1;',
// Remove the line with string form of `output`
output: 'const b = 1;',
// Add the line with array form of `output`
output: ['const b = 1;', 'const c = 1;'],
errors: [
/* ... */
],
},
],
});

See [rule-tester] support multipass fixes for more details.

Other Developer-Facing Breaking Changes

Appreciation

We'd like to extend a sincere thank you to everybody who pitched in to make typescript-eslint v8 possible.

See the v8.0.0 milestone for the list of issues and associated merged pull requests.

Supporting typescript-eslint

If you enjoyed this blog post and/or use typescript-eslint, please consider supporting us on Open Collective. We're a small volunteer team and could use your support to make the ESLint experience on TypeScript great. Thanks! 💖