Bug: typescript-eslint config types are incompatible with `defineConfig()` types
Before You File a Proposal Please Confirm You Have Done The Following...
- [x] I have searched for related issues and found none that match my proposal.
- [x] I have read the FAQ and my problem is not listed.
Description
I just installed eslint with typescript-eslint (npm init @eslint/config@latest) - I haven't done anything yet - right away I get this error.
Type '({ readonly rules: Readonly<RulesRecord>; } | Config)[]' is not assignable to type 'Config<RulesRecord>[]'.
Type '{ readonly rules: Readonly<RulesRecord>; } | Config' is not assignable to type 'Config<RulesRecord>'.
Type 'Config' is not assignable to type 'Config<RulesRecord>'.
Types of property 'languageOptions' are incompatible.
Type 'import("/media/neuronet/projekty/event-conductor/node_modules/@typescript-eslint/utils/dist/ts-eslint/Config").FlatConfig.LanguageOptions | undefined' is not assignable to type 'import("/media/neuronet/projekty/event-conductor/node_modules/eslint/lib/types/index").Linter.LanguageOptions | undefined'.
Type 'import("/media/neuronet/projekty/event-conductor/node_modules/@typescript-eslint/utils/dist/ts-eslint/Config").FlatConfig.LanguageOptions' is not assignable to type 'import("/media/neuronet/projekty/event-conductor/node_modules/eslint/lib/types/index").Linter.LanguageOptions'.
Types of property 'parser' are incompatible.
Type 'LooseParserModule | undefined' is not assignable to type 'Parser | undefined'.
Type '{ meta?: { name?: string | undefined; version?: string | undefined; } | undefined; parseForESLint(text: string, options?: unknown): { ast: unknown; scopeManager?: unknown; services?: unknown; visitorKeys?: unknown; }; }' is not assignable to type 'Parser | undefined'.
Type '{ meta?: { name?: string | undefined; version?: string | undefined; } | undefined; parseForESLint(text: string, options?: unknown): { ast: unknown; scopeManager?: unknown; services?: unknown; visitorKeys?: unknown; }; }' is not assignable to type 'Omit<ESTreeParser, "parseForESLint"> & { parseForESLint(text: string, options?: any): Omit<ESLintParseResult, "ast" | "scopeManager"> & { ...; }; }'.
Type '{ meta?: { name?: string | undefined; version?: string | undefined; } | undefined; parseForESLint(text: string, options?: unknown): { ast: unknown; scopeManager?: unknown; services?: unknown; visitorKeys?: unknown; }; }' is not assignable to type '{ parseForESLint(text: string, options?: any): Omit<ESLintParseResult, "ast" | "scopeManager"> & { ast: unknown; scopeManager?: unknown; }; }'.
The types returned by 'parseForESLint(...)' are incompatible between these types.
Type '{ ast: unknown; scopeManager?: unknown; services?: unknown; visitorKeys?: unknown; }' is not assignable to type 'Omit<ESLintParseResult, "ast" | "scopeManager"> & { ast: unknown; scopeManager?: unknown; }'.
Type '{ ast: unknown; scopeManager?: unknown; services?: unknown; visitorKeys?: unknown; }' is not assignable to type 'Omit<ESLintParseResult, "ast" | "scopeManager">'.
Types of property 'visitorKeys' are incompatible.
Type 'unknown' is not assignable to type 'VisitorKeys | undefined'.ts(2322)
Impacted Configurations
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
/** @type {import('eslint').Linter.Config[]} */
export default [
{ files: ["**/*.{js,mjs,cjs,ts}"] },
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
];
Additional Info
"typescript-eslint": "^8.25.0"
"@eslint/js": "^9.21.0",
"eslint": "^9.21.0",
I am using vscode with: https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint @ latest https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode @ latest
I have read that the problem is with the eslint types (#10872) but maybe you will find another way to fix this - so I leave this bug here :P
I just ran into the same problem trying to enable @ts-check for my eslint.config.mjs file.
👍 Minimum reproduction here: https://github.com/JoshuaKGoldberg/repros/tree/eslint-defineconfig-with-typescript-eslint
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
export default defineConfig(tseslint.configs.recommended);
Noting for reference that when this gets worked on, we can also switch ourselves internally to defineConfig() and unblock https://github.com/typescript-eslint/typescript-eslint/issues/10935.
All right, so the trouble here seems to be that the configs that we export are of our own FlatConfig.Config type...
https://github.com/typescript-eslint/typescript-eslint/blob/69e2f6c0d371f304c6793ba1801adde10a89372b/packages/eslint-plugin/raw-plugin.d.ts#L5-L25
... which is (intentionally) loose, e.g.
https://github.com/typescript-eslint/typescript-eslint/blob/6250dabd3048eab32d7624ab8d697a6e09bb6c9c/packages/utils/src/ts-eslint/Parser.ts#L19-L52
... in ways that unfortunately are incompatible with eslint's defineConfig(). Hence the eventual unknown not being assignable to VisitorKeys | undefined way down the stack. Resolving this specific incompatibility uncovers some others. Also, the typescript-eslint Plugin type isn't assignable to eslint's Plugin type either for similar reasons....
So, our options look roughly like:
-
Make the typescript-eslint types stricter, so that they are at least assignable to the eslint core types.
- This may be technically a breaking change for other projects using our types to author plugins/configs.
-
Leave the typescript-eslint types as-is, but use eslint core's types for our exported configs and plugin.
-
I've prototyped this at https://github.com/typescript-eslint/typescript-eslint/pull/11190
-
This makes
tseslint.configs.*andtseslint.plugincompatible withdefineConfig()asap but may mean that other projects using our types will still have compatibility problems withdefineConfig(). -
Also, this probably means we need to add an explicit dependency on the eslint types somehow, rather than only consuming having eslint as a peer dependency (currently
^8.57.0 || ^9.0.0). Maybe if we get the types directly out of@eslint/config-helperswith^0.2.0this is ok though?
-
-
Potentially request changes upstream if the upstream types are deemed problematic / too strict.
Thoughts? @typescript-eslint/triage-team , especially @bradzacher
So the reason that the types we use for the config function are loose is because we want to be more permissive in what the types allow. Essentially the goal was to provide validation of user-configurable things without enforcing that 3rd-party packages played nice with the types.
So for example we have strict types for languageOptions or rules because those are directly user-configurable. However we don't enforce that plugins and parsers exactly match the required shape (which is why the types make heavy use of unknown).
Why do we do this? Well we don't want some 3rd party package to release with bad types and have that cause issues for users. The package's types might be poorly written, or they might be out of date, or they might be written for a newer version of ESLint than the user uses, or they might not even have types (so you're at the mercy of what TS infers from the JS code). Either way we don't want that cruft to impact a user's config typechecking -- so we just define types that describe generalties like "a parser must have a parse function or a parseForESLint function" without enforcing further than that.
However we definitely shouldn't be using these loose types for our exported configs -- the loose types should only be used for the tseslint.config argument.
Ok, so while digging into this I have come across one "unsolvable" type issue so far. the ESLint types require a boolean for rule.meta.docs.recommended (presumably as a result of how they use the field internally). We, however, have our rules provide different runtime types in that position in order to generate the strict configs with possibly different rule options.
Minimal repro (TS Playground):
import { defineConfig } from 'eslint/config';
defineConfig({
plugins: {
'some-plugin': {
rules: {
'some-rule': {
meta: {
docs: {
recommended: 'not a boolean!'
}
},
create() {
return {};
}
}
}
}
}
})
Examples where we use this type of thing:
https://github.com/typescript-eslint/typescript-eslint/blob/63ab00225eea8c9e4c8fadc22d5c3081daf13e18/packages/eslint-plugin/src/rules/await-thenable.ts#L31
https://github.com/typescript-eslint/typescript-eslint/blob/a43c19969552a8e7f74151562709bc736b99e9d6/packages/eslint-plugin/src/rules/return-await.ts#L40-L42
Of course, defineConfig's types being strict in this way doesn't have any runtime benefit, since they won't use this data at all within eslint. Perhaps we should request more lenient types from upstream in order to be compatible? Or perhaps we're best off just doing "Omit<typeof rules, 'meta'>" for the purposes of this issue? Alternately we could make runtime changes to move off of the recommended field altogether and use any other name, e.g. tseslintRecommended, in order to avoid the clash?
That seems to me to be a bug in the ESLint types. Forcing non-core rules to use boolean for meta.docs.recommended is not documented as a preference anywhere. Based on conversation in https://github.com/eslint/rfcs/pull/132 it seems to generally be a type that each plugin should set for itself.
We've had to fix roughly the same bug in tseslint: #8695.
Filed https://github.com/eslint/rewrite/issues/234 👍
Another fun one: https://github.com/eslint/eslint/issues/19903
I think I personally have to give up on this one :tired_face:. I've spent quite a bit of time, and just repeatedly hit daunting roadblocks. If anyone else wants to give it a stab, please do, and please plagiarize anything of mine that's useful to you!
Fundamentally, we are looking for the following type tests to pass
// with latest eslint
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
defineConfig({
plugins: {
"@typescript-eslint": tseslint.plugin,
},
});
defineConfig(tseslint.configs.recommended /* and the rest of the configs */);
defineConfig({
languageOptions: {
parser: tseslint.parser,
},
});
and
// with latest eslint AND with eslint 8.57.0
import tseslint from "typescript-eslint";
tseslint.config({
plugins: {
"@typescript-eslint": tseslint.plugin,
},
});
tseslint.config(tseslint.configs.recommended /* and the rest of the configs */);
tseslint.config({
languageOptions: {
parser: tseslint.parser,
},
});
Note that my assessment is that the eslint 8.57.0 compatibility requirement all but guarantees we cannot reuse any relevant types authored natively by eslint. This is exacerbated by https://github.com/eslint/rewrite/issues/226.
I get a very similar error with "@eslint/js": "^9.33.0", "eslint": "^9.33.0", "typescript-eslint": "^8.39.0",:
eslint.config.mjs:20:5 - error TS2345: Argument of type 'Config' is not assignable to parameter of type 'InfiniteArray<ConfigWithExtends>'.
Type 'Config' is not assignable to type 'ConfigWithExtends' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
The types of 'languageOptions.parser' are incompatible between these types.
Type 'LooseParserModule | undefined' is not assignable to type 'Parser | undefined'.
Type '{ meta?: { name?: string | undefined; version?: string | undefined; }; parseForESLint(text: string, options?: unknown): { ast: unknown; scopeManager?: unknown; services?: unknown; visitorKeys?: unknown; }; }' is not assignable to type 'Parser | undefined'.
Type '{ meta?: { name?: string | undefined; version?: string | undefined; }; parseForESLint(text: string, options?: unknown): { ast: unknown; scopeManager?: unknown; services?: unknown; visitorKeys?: unknown; }; }' is not assignable to type 'ObjectMetaProperties & { parseForESLint(text: string, options?: any): ESLintParseResult; }' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
Type '{ meta?: { name?: string | undefined; version?: string | undefined; }; parseForESLint(text: string, options?: unknown): { ast: unknown; scopeManager?: unknown; services?: unknown; visitorKeys?: unknown; }; }' is not assignable to type '{ parseForESLint(text: string, options?: any): ESLintParseResult; }'.
The types of 'parseForESLint(...).ast' are incompatible between these types.
Type 'unknown' is not assignable to type 'Program'.
20 ...tsEslint.config({
~~~~~~~~~~~~~~~~~~~~
21 files: ["**/*.d.ts", "**/*.ts", "**/*.mts", "**/*.mjs", "**/*.tsx", "**/*.jsx"],
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
...
37 }
~~~~~~~~~
38 }),
~~~~~~