typescript-eslint icon indicating copy to clipboard operation
typescript-eslint copied to clipboard

Bug: typescript-eslint config types are incompatible with `defineConfig()` types

Open neuronetio opened this issue 10 months ago • 5 comments

Before You File a Proposal Please Confirm You Have Done The Following...

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

neuronetio avatar Feb 27 '25 17:02 neuronetio

I just ran into the same problem trying to enable @ts-check for my eslint.config.mjs file.

gtbuchanan avatar Mar 04 '25 16:03 gtbuchanan

👍 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);

JoshuaKGoldberg avatar Mar 31 '25 13:03 JoshuaKGoldberg

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.

kirkwaiblinger avatar May 05 '25 21:05 kirkwaiblinger

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:

  1. 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.
  2. 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.* and tseslint.plugin compatible with defineConfig() asap but may mean that other projects using our types will still have compatibility problems with defineConfig().

    • 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-helpers with ^0.2.0 this is ok though?

  3. Potentially request changes upstream if the upstream types are deemed problematic / too strict.


Thoughts? @typescript-eslint/triage-team , especially @bradzacher

kirkwaiblinger avatar May 08 '25 00:05 kirkwaiblinger

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.

bradzacher avatar May 08 '25 05:05 bradzacher

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?

kirkwaiblinger avatar Jun 30 '25 20:06 kirkwaiblinger

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.

JoshuaKGoldberg avatar Jun 30 '25 20:06 JoshuaKGoldberg

Filed https://github.com/eslint/rewrite/issues/234 👍

kirkwaiblinger avatar Jun 30 '25 21:06 kirkwaiblinger

Another fun one: https://github.com/eslint/eslint/issues/19903

kirkwaiblinger avatar Jun 30 '25 22:06 kirkwaiblinger

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.

kirkwaiblinger avatar Jul 01 '25 01:07 kirkwaiblinger

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     }),
    ~~~~~~

jendrikw avatar Aug 13 '25 09:08 jendrikw