Skip to content

Bug: exactOptionalPropertyTypes causes type errors when using plugins that were not built with this option #402

@sebastian-altamirano

Description

@sebastian-altamirano

Environment

Node version: v22.15.0
npm version: v11.3.0
Local ESLint version: v9.27.0 (Currently used)
Global ESLint version: Not found
Operating System: linux 5.15.167.4-microsoft-standard-WSL2

What parser are you using?

Default (Espree)

What did you do?

I have installed some plugins and configured ESLint to use them. ESLint works fine, but TypeScript throws some type errors related to exactOptionalPropertyTypes, which I have set to true in my tsconfig.json since I'm extending from @tsconfig/strictest.

What did you expect to happen?

There should be no type errors when using exactOptionalPropertyTypes.

What actually happened?

Type errors occurred.

Link to Minimal Reproducible Example

https://stackblitz.com/edit/vitejs-vite-gantpukn?file=eslint.config.mjs,tsconfig.json&view=editor

Participation

  • I am willing to submit a pull request for this issue.

Additional comments

I have 2 reproductions:

According to @nzakas: "The Plugin type exported from eslint is the thing that needs updating to address this." (#391 (comment))

Activity

sebastian-altamirano

sebastian-altamirano commented on May 28, 2025

@sebastian-altamirano
Author

If exactOptionalPropertyTypes is set to false at https://stackblitz.com/edit/stackblitz-starters-rh2kntpu?file=eslint.config.mjs,tsconfig.config.json&view=editor, there are still type errors.

These are for a different issue, but I’m unsure whether they fall under ESLint’s responsibility, the plugins' responsibility, or if my configuration needs adjustments. Any thoughts on where this should be addressed so that I can create the corresponding issues?

moved this from Needs Triage to Triaging in Triageon May 28, 2025
added
repro:yesIssues with a reproducible example
and removed
repro:neededThis issue should include a reproducible example
on May 28, 2025
fasttime

fasttime commented on May 28, 2025

@fasttime
Member

Thanks for the issue @sebastian-altamirano. I can see that the @eslint/markdown types for the processor config include some optional properties with value undefined, which are inconsistent with how configs in ESLit are typed.

from https://app.unpkg.com/@eslint/markdown@6.4.0/files/dist/esm/index.d.ts#L184-213:

        processor: ({
            name: string;
            plugins: {};
            files?: undefined;
            processor?: undefined;
            languageOptions?: undefined;
            rules?: undefined;
        } | {
            name: string;
            files: string[];
            processor: string;
            plugins?: undefined;
            languageOptions?: undefined;
            rules?: undefined;
        } | {
            name: string;
            files: string[];
            languageOptions: {
                parserOptions: {
                    ecmaFeatures: {
                        impliedStrict: boolean;
                    };
                };
            };
            rules: {
                [rule: string]: import("eslint").Linter.RuleEntry<any[]>;
            };
            plugins?: undefined;
            processor?: undefined;
        })[];

Note that files, processor, languageOptions, rules, plugins are explicitly typed with the value undefined in some of the config objects. To me, this looks like a tsc compiler artifact given that none of these properties are specified as undefined in the source code (https://github.com/eslint/markdown/blob/v6.4.0/src/index.js#L108-L134).

			{
				name: "markdown/recommended/plugin",
				plugins: (processorPlugins = {}),
			},
			{
				name: "markdown/recommended/processor",
				files: ["**/*.md"],
				processor: "markdown/markdown",
			},
			{
				name: "markdown/recommended/code-blocks",
				files: ["**/*.md/**"],
				languageOptions: {
					parserOptions: {
						ecmaFeatures: {
							// Adding a "use strict" directive at the top of
							// every code block is tedious and distracting, so
							// opt into strict mode parsing without the
							// directive.
							impliedStrict: true,
						},
					},
				},
				rules: {
					...processorRulesConfig,
				},
			},
moved this from Triaging to Evaluating in Triageon May 28, 2025
fasttime

fasttime commented on May 28, 2025

@fasttime
Member

If exactOptionalPropertyTypes is set to false at https://stackblitz.com/edit/stackblitz-starters-rh2kntpu?file=eslint.config.mjs,tsconfig.config.json&view=editor, there are still type errors.

I tried running npm run check-eslint-config in the above repro, and I'm getting the same error messages with or without the exactOptionalPropertyTypes option set, so yes, these don't seem related to the behavior of that option in particular.

These are for a different issue, but I’m unsure whether they fall under ESLint’s responsibility, the plugins' responsibility, or if my configuration needs adjustments. Any thoughts on where this should be addressed so that I can create the corresponding issues?

If you encounter any incompatibilities with third party plugins it's better to file a bug at the plugin's maintainers, because ESLint doesn't provide the types for those plugins, and we can't support tools that we don't maintain.

Now, in the case of the eslint.config.mjs file in the StackBlitz repro, the compiler errors arise from the fact that some of the plugins in that config (angular-eslint, eslint-plugin-import-x, @smarttools/eslint-plugin-rxjs, typescript-eslint and possibly more) use type definitions provided by TSESlint, which are not compatible with native ESLint types. We had reports of similar incompatibilities in the past (e.g. eslint/eslint#19467). The inconsistencies are partly due to historical reasons, because typescript-eslint provided types for the flat config API before ESLint did.

sebastian-altamirano

sebastian-altamirano commented on May 28, 2025

@sebastian-altamirano
Author

Makes sense! Since @eslint/markdown does not add a type anotation for the plugin constant, TypeScript does its best to infer one, resulting in that unusual type with undefined values. Given this, I believe my original PR #391 makes more sense now.

Now, in the case of the eslint.config.mjs file in the StackBlitz repro, the compiler errors arise from the fact that some of the plugins in that config (angular-eslint, eslint-plugin-import-x, @smarttools/eslint-plugin-rxjs, typescript-eslint and possibly more) use type definitions provided by TSESlint, which are not compatible with native ESLint types. We had reports of similar incompatibilities in the past (e.g. eslint/eslint#19467). The inconsistencies are partly due to historical reasons, because typescript-eslint provided types for the flat config API before ESLint did.

I did not know that! So that's why there are no errors when using tseslint.config().

fasttime

fasttime commented on Jun 2, 2025

@fasttime
Member

Sorry for the delay! I think there isn't much we can do address the incompatibilities with third-party plugins because of the above reasons. As for the issue with the eslint/markdown types, it should be probably addressed in that repo. The problem is that we don't use the exactOptionalPropertyTypes option to build the type declarations, and tsc creates types that are perfectly valid without that option. There are a few ways to fix this issue. Without adding excessive complexity to the build process, I can think of at least three solutions:

Type config explicitly

This approach would consist in coercing the type of the recommended-legacy config to LegacyConfig, and the type of the processor and recommended configs to Config[]. This can be achieved with a @type tag annotation as you did in #391 e.g.:

    		processor: /** @type {Config[]} */ ([
			{
				name: "markdown/recommended/plugin",
				plugins: (processorPlugins = {}),
			},
			...
		),

The resulting type will be:

        processor: Config[];

This seems safe as the elements of individual config objects are not typically accessed directly.

Use a helper function

Another option is using a helper function to type the processor config as a tuple rather than as an array, as suggested in microsoft/TypeScript#27179 (comment):

/**
 * @param {T} value
 * @template {unknown[]} T
 * @returns {T}
 */
function tuple(...value) {
	return value;
}

...

		processor: tuple(
			{
				name: "markdown/recommended/plugin",
				plugins: (processorPlugins = {}),
			},
			...
		),

The generated types will look like:

        processor: [{
            name: string;
            plugins: {};
        },
        ...
        ]

This is more correct than manual typing with Config[], but adds a function call.

Build type declarations with exactOptionalPropertyTypes

The exactOptionalPropertyTypes option requires strictNullChecks, which we don't use, because it would generate errors. The errors can be disabled with noCheck, but that would also turn off errors that we don't want to miss upon. This means that in order to fail the build with an error if the types aren't correct, we'll have to run tsc another time without noCheck but with noEmit, so that no types are generated. The whole command line would be like this:

npm run build:rules && rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json --noEmit && tsc -p tsconfig.esm.json --noCheck --exactOptionalPropertyTypes --strictNullChecks && npm run build:update-rules-docs

And the generated type for the processor config:

        processor: ({
            name: string;
            plugins: {};
            files?: never;
            processor?: never;
            languageOptions?: never;
            rules?: never;
        } |
        ...
        )

Note that undefined is now replaced by never. This is the most robust solution as it would avoid future incompatibilities that could arise because of exactOptionalPropertyTypes in that plugin.

31 remaining items

eslint-github-bot

eslint-github-bot commented on Sep 14, 2025

@eslint-github-bot

👋 Hi! This issue is being addressed in pull request #524. Thanks, @lumirlumir!

eslint-github-bot

eslint-github-bot commented on Sep 14, 2025

@eslint-github-bot

👋 Hi! This issue is being addressed in pull request #524. Thanks, @lumirlumir!

lumirlumir

lumirlumir commented on Sep 14, 2025

@lumirlumir
Member

It took quite a while to find the right solution for this, but I was able to figure it out and have opened a PR with the fix.

If it's alright, I am assigning myself to this issue since it's been some time since we last discussed solutions for it.

eslint-github-bot

eslint-github-bot commented on Sep 15, 2025

@eslint-github-bot

👋 Hi! This issue is being addressed in pull request #524. Thanks, @lumirlumir!

eslint-github-bot

eslint-github-bot commented on Sep 15, 2025

@eslint-github-bot

👋 Hi! This issue is being addressed in pull request #524. Thanks, @lumirlumir!

eslint-github-bot

eslint-github-bot commented on Sep 15, 2025

@eslint-github-bot

👋 Hi! This issue is being addressed in pull request #524. Thanks, @lumirlumir!

eslint-github-bot

eslint-github-bot commented on Sep 15, 2025

@eslint-github-bot

👋 Hi! This issue is being addressed in pull request #524. Thanks, @lumirlumir!

moved this from Feedback Needed to Complete in Triageon Sep 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Stalebugrepro:yesIssues with a reproducible example

Type

No type

Projects

Status

Complete

Milestone

No milestone

Relationships

None yet

    Participants

    @nzakas@JoshuaKGoldberg@fasttime@sebastian-altamirano@mdjermanovic

    Issue actions

      Bug: `exactOptionalPropertyTypes` causes type errors when using plugins that were not built with this option · Issue #402 · eslint/markdown