Announcing typescript-eslint v8
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
:
- Flat Config
- Legacy Config
npm i typescript-eslint@8 --save-dev
npm i @typescript-eslint/eslint-plugin@8 @typescript-eslint/parser@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
:
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 ondefaultProject
: path to a TypeScript configuration file to use for the slower default project
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!
If your ESLint configuration contains many rules
configurations, we suggest the following strategy to start anew:
- Remove all your rules configurations
- Extend from the preset configs that make sense for you
- Run ESLint on your project
- In your ESLint configuration, turn off any rules creating errors that don't make sense for your project — with comments explaining why
- 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:
- Rules: Deprecate prefer-ts-expect-error in favor of ban-ts-comment
- If you have
@typescript-eslint/prefer-ts-expect-error
manually enabled, remove that, and instead either use a recommended config or manually enable@typescript-eslint/ban-ts-comment
.
- If you have
- chore(eslint-plugin): deprecate no-var-requires in favor of no-require-imports
- If you have
@typescript-eslint/no-var-requires
manually enabled, remove that, and instead either use a recommended config or manually enable@typescript-eslint/no-require-imports
.
- If you have
- feat(eslint-plugin): remove deprecated no-throw-literal rule
- If you have
@typescript-eslint/no-throw-literal
manually enabled, remove that, and instead either use a recommended config or manually enable@typescript-eslint/only-throw-error
.
- If you have
- feat(eslint-plugin): [no-useless-template-literals] rename to no-useless-template-expression (deprecate no-useless-template-literals) and fix: no-useless-template-expression -> no-unnecessary-template-expression
- Find-and-replace text from
no-useless-template-literals
tono-unnecessary-template-expression
- Find-and-replace text from
- feat(eslint-plugin): deprecate no-loss-of-precision extension rule
- If you have
@typescript-eslint/no-loss-of-precision
manually enabled, replace it with the base ruleno-loss-of-precision
.
- If you have
- feat(eslint-plugin): remove formatting/layout rules
- If you're using any of the old deprecated formatting rules, see eslint.style for their new equivalents
- feat(eslint-plugin): [prefer-nullish-coalescing] change ignoreConditionalTests default to true
- If you want to have the rule check conditional tests, set its
ignoreConditionalTests
option tofalse
in your ESLint config
- If you want to have the rule check conditional tests, set its
- feat(eslint-plugin): [no-unused-vars] align catch behavior to ESLint 9
- If you want
@typescript-eslint/no-unused-vars
to ignore caught errors, enable itscaughtErrors
option to'none'
in your ESLint config
- If you want
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
andNumber
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:
@typescript-eslint/no-restricted-types
is the new rule for banning a configurable list of type names. It has no options enabled by default.@typescript-eslint/no-empty-object-type
bans the built-in{}
type in confusing locations.@typescript-eslint/no-unsafe-function-type
bans the built-inFunction
type@typescript-eslint/no-wrapper-object-types
bansObject
and built-in class wrappers such asNumber
.
To migrate to the new rules:
- If you were disabling the ban on
{}
, consider enabling@typescript-eslint/no-empty-object-type
, as it allows some cases of{}
that were previously banned. - If you were banning any configurable types lists, provide a similar configuration to
no-restricted-types
. - If you have
@typescript-eslint/ban-types
manually enabled, it will no longer ban:{}
orobject
: use a recommended config or manually enable@typescript-eslint/no-empty-object-type
.Function
: use a recommended config or manually enable@typescript-eslint/no-unsafe-function-type
.Number
or other built-in uppercase types: use a recommended config or manually enable@typescript-eslint/no-wrapper-object-types
.
- If you have
@typescript-eslint/no-empty-interface
manually enabled, remove that, and instead either use a recommended config or manually enable@typescript-eslint/no-empty-object-type
.
For more details, see the issues and pull requests that split apart the ban-types
rule:
- Enhancement: [ban-types] Split the ban into a separate, better-phrased rule
- feat(eslint-plugin): split no-empty-object-type out from ban-types and no-empty-interface
- Enhancement: [ban-types] Split into default-less no-restricted-types and more targeted type ban rule(s)
- feat(eslint-plugin): replace ban-types with no-restricted-types, no-unsafe-function-type, no-wrapper-object-types
Tooling Breaking Changes
- fix(typescript-estree): enable dot globs for project by default
- This will cause any
parserOptions.project
globs to match dot (.
) directories. If you don't want to match them then use a more specific set of globs, or switch toparserOptions.projectService
.
- This will cause any
- feat(typescript-estree): remove slow deprecated and isolated programs
- If you were still using
parserOptions.DEPRECATED__createDefaultProgram
, switch toparserOptions.projectService
(recommended) orparserOptions.project
.
- If you were still using
- feat(typescript-estree): rename automaticSingleRunInference to disallowAutomaticSingleRunInference
- We've updated the default to be an opt-out - meaning you no longer need to enable it:
parserOptions: {
automaticSingleRunInference: true,
// ...
}
- We've updated the default to be an opt-out - meaning you no longer need to enable it:
- chore: bump minimum versions for v8
- ESLint support range was changed from
^8.56.0
to^8.57.0
- Node.js support range was changed from
^18.18.0 || >=20.0.0
to^18.18.0 || ^20.9.0 || >=21.1.0
- TypeScript support range was changed from
>=4.7.4 <5.5.0
to>=4.8.4 <5.5.0
- ESLint support range was changed from
- Parser: remove EXPERIMENTAL_useSourceOfProjectReferenceRedirect in favor of project service
- We now recommend using the new
parserOptions.projectService
instead
- We now recommend using the new
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.
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.
- Enhancement: add strict parent types for nodes that have well-defined parents
- This will help you remove some unnecessary conditions - we suggest using
@typescript-eslint/no-unnecessary-condition
to help find the unnecessary checks!
- This will help you remove some unnecessary conditions - we suggest using
- feat(typescript-estree): split TSMappedType typeParameter into constraint and key
- If your code handles mapped types, change from
node.typeParameter.constraint
tonode.constraint
and fromnode.typeParameter.name
tonode.key
- If your code handles mapped types, change from
- feat(ast-spec): remove deprecated type params
- If you haven't already you must stop using those removed AST properties that were already marked as
@deprecated
- If you haven't already you must stop using those removed AST properties that were already marked as
- fix(typescript-estree): add TSEnumBody node for TSEnumDeclaration body #8920
- If your code handles enums, switch from
node.members
tonode.body.members
- If your code handles enums, switch from
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
andurl
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:
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:
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
: from5
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:
import { RuleTester } from '@typescript-eslint/rule-tester
const ruleTester = new RuleTester();
const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
});
For more details, see:
- ESLint > Migrate to 9.0.0 >
FlatRuleTester
is nowRuleTester
- typescript-eslint > feat(rule-tester): switched to flat config
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
- feat(parser): always enable comment, loc, range, tokens
- If you were manually calling
@typescript-eslint/parser
functions, those options are no longer necessary to provide
- If you were manually calling
- chore(type-utils)!: remove IsNullableTypeOptions interface
- If you were using
isNullableType
, you can omit its section parameter
- If you were using
- feat(utils): swap LegacyESLint out for FlatESLint as ESLint export
- If you still need to use the class corresponding to legacy ("eslintrc") configs, switch from importing
ESLint
toLegacyESLint
- If you still need to use the class corresponding to legacy ("eslintrc") configs, switch from importing
- chore(type-utils): remove getTypeArguments
- If you were using
getTypeArguments
, call a TypeScript type checker'schecker.getTypeArguments
instead
- If you were using
- feat(utils): remove deprecated context helpers
- You should consider dropping support for older ESLint versions and migrate to the new APIs.
Appreciation
We'd like to extend a sincere thank you to everybody who pitched in to make typescript-eslint v8 possible.
- Ourselves on the maintenance team:
- Community contributors whose PRs were merged into the 8.0.0 release:
- Community maintainers and projects who worked with us to try out the v8 beta:
- Babel with Nicolò Ribaudo
- create-t3-app with Julius Marminge
- postcss-plugins with Romain Menke
- Prettier with fisker Cheung
- RedwoodJS with Tobbe Lundberg
- SvelteKit with Ben McCann
- tRPC with Alex Katt
- Vite with Bjorn Lu
- Community plugins and projects who worked with us to try out the v8 beta:
- Members of the TypeScript team who helped with performance issues:
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! 💖