eslint-plugin-react icon indicating copy to clipboard operation
eslint-plugin-react copied to clipboard

[Bug]: TS types are broken

Open burtek opened this issue 1 year ago • 8 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues and my issue is unique
  • [X] My issue appears in the command-line and not only in the text editor

Description Overview

The types as exported in #3797 and #3836 are broken and throw TS errors while used. The package only exports named namespace of configs, which means you can't do any of the following:

import react from 'eslint-plugin-react';
import { config } from 'typescript-eslint';

export default config({
    plugin: { react },
    rules: {
        ...react.configs.recommended.rules,
        ...someMoreRules
    }
});

image

https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBFApgQwMbwGZQiOByRAZwBtgA7GAWjGIFcBzcypNGPAbgChRJY4BvOKghkMwenAC+cLDnwwAnmCKoowMFSKkKHTp2FlC8QjkQBZaIgBKtYkTgBeAZK6dEAD17wAJogzJbeAMxegAKfk44KLgaBnJCAC4BBBR0KQAaSOioWyIkiOjCuAA6UpZ0YuDxQmKkYRAQRDJfb1rcwkyi6NLik0aLJBs7QiyoyU5JAEp2IA

Expected Behavior

No TS error.

eslint-plugin-react version

v7.37.1

eslint version

v8.57.0

node version

v20.9.0

typescript version

All, tested on v5.5.2, v.5.6.2

burtek avatar Oct 01 '24 18:10 burtek

This appears to be because only the flat configs are included in the types. You can do reactPlugin.configs.flat.recommended, but you can't access the legacy configs with reactPlugin.configs.recommended.

JstnMcBrd avatar Oct 01 '24 20:10 JstnMcBrd

Oh, that's true (though still not in line with what's actually exported). But I still can't do plugin: { react }

burtek avatar Oct 01 '24 20:10 burtek

And the flat config is not typescript-eslint compatible either...

image

burtek avatar Oct 01 '24 20:10 burtek

Yes, I was able to add that - even the flat config types are broken. They appear to be incompatible with ESLint's types.

import type { Linter } from 'eslint';

export function reactConfig(): Linter.FlatConfig[] {
  return [
    reactPlugin.configs.flat.recommended,
    reactPlugin.configs.flat['jsx-runtime'],
  ];
}
Type '{ plugins: { react: { deprecatedRules: any; rules: { 'boolean-prop-naming': RuleModule; 'button-has-type': RuleModule; 'checked-requires-onchange-or-readonly': RuleModule; ... 99 more ...; 'void-dom-elements-no-children': RuleModule; }; configs: { ...; }; }; }; rules: { ...; }; languageOptions: { ...; }; }' is not assignable to type 'FlatConfig<RulesRecord>'.
  Types of property 'plugins' are incompatible.
    Type '{ react: { deprecatedRules: any; rules: { 'boolean-prop-naming': RuleModule; 'button-has-type': RuleModule; 'checked-requires-onchange-or-readonly': RuleModule; ... 99 more ...; 'void-dom-elements-no-children': RuleModule; }; configs: { ...; }; }; }' is not assignable to type 'Record<string, Plugin>'.
      Property 'react' is incompatible with index signature.
        Type '{ deprecatedRules: any; rules: { 'boolean-prop-naming': RuleModule; 'button-has-type': RuleModule; 'checked-requires-onchange-or-readonly': RuleModule; ... 99 more ...; 'void-dom-elements-no-children': RuleModule; }; configs: { ...; }; }' is not assignable to type 'Plugin'.
          Types of property 'configs' are incompatible.
            Type '{ recommended: { plugins: string[]; parserOptions: { ecmaFeatures: { jsx: boolean; }; }; rules: { 'react/display-name': number; 'react/jsx-key': number; 'react/jsx-no-comment-textnodes': number; 'react/jsx-no-duplicate-props': number; ... 17 more ...; 'react/require-render-return': number; }; }; all: { ...; }; 'jsx-...' is not assignable to type 'Record<string, FlatConfig<RulesRecord> | FlatConfig<RulesRecord>[] | ConfigData<RulesRecord>>'.
              Property 'recommended' is incompatible with index signature.
                Type '{ plugins: string[]; parserOptions: { ecmaFeatures: { jsx: boolean; }; }; rules: { 'react/display-name': number; 'react/jsx-key': number; 'react/jsx-no-comment-textnodes': number; 'react/jsx-no-duplicate-props': number; ... 17 more ...; 'react/require-render-return': number; }; }' is not assignable to type 'FlatConfig<RulesRecord> | FlatConfig<RulesRecord>[] | ConfigData<RulesRecord>'.
                  Type '{ plugins: string[]; parserOptions: { ecmaFeatures: { jsx: boolean; }; }; rules: { 'react/display-name': number; 'react/jsx-key': number; 'react/jsx-no-comment-textnodes': number; 'react/jsx-no-duplicate-props': number; ... 17 more ...; 'react/require-render-return': number; }; }' is not assignable to type 'ConfigData<RulesRecord>'.
                    Types of property 'rules' are incompatible.
                      Type '{ 'react/display-name': number; 'react/jsx-key': number; 'react/jsx-no-comment-textnodes': number; 'react/jsx-no-duplicate-props': number; 'react/jsx-no-target-blank': number; 'react/jsx-no-undef': number; ... 15 more ...; 'react/require-render-return': number; }' is not assignable to type 'Partial<RulesRecord>'.
                        Property ''react/display-name'' is incompatible with index signature.
                          Type 'number' is not assignable to type 'RuleEntry<any[]> | undefined'.ts(2322)

On first glance, I wonder if this is because the type appears to have some recursion. It's possible to do reactPlugin.configs.flat.recommended.plugins.react.configs.recommended.

image

The flat configs contain the legacy configs. Is that intentional? That might be the problem.

JstnMcBrd avatar Oct 01 '24 20:10 JstnMcBrd

@JstnMcBrd yes, that's intentional and unavoidable with the current architecture of the plugin.

ljharb avatar Oct 02 '24 08:10 ljharb

I can probably look into fixing types today or tomorrow, no promises though

EDIT: so far wasn't able to

burtek avatar Oct 02 '24 08:10 burtek

I am having the same issue.

      react: eslintPluginReact,
Type '{ deprecatedRules: any; rules: { 'boolean-prop-naming': Rule.RuleModule; 'button-has-type': Rule.RuleModule; 'checked-requires-onchange-or-readonly': Rule.RuleModule; ... 99 more ...; 'void-dom-elements-no-children': Rule.RuleModule; }; configs: { ...; }; }' is not assignable to type 'Omit<Plugin, "configs">'.
  Types of property 'rules' are incompatible.
    Type '{ 'boolean-prop-naming': Rule.RuleModule; 'button-has-type': Rule.RuleModule; 'checked-requires-onchange-or-readonly': Rule.RuleModule; 'default-props-match-prop-types': Rule.RuleModule; ... 98 more ...; 'void-dom-elements-no-children': Rule.RuleModule; }' is not assignable to type 'Record<string, LooseRuleDefinition>'.
      Property ''jsx-no-literals'' is incompatible with index signature.
        Type '{ meta: RuleModule["meta"]; create(context: any): (false | { ImportDeclaration(node: any): void; VariableDeclaration(node: any): void; }) & { ...; }; }' is not assignable to type 'LooseRuleDefinition'.
          Type '{ meta: RuleModule["meta"]; create(context: any): (false | { ImportDeclaration(node: any): void; VariableDeclaration(node: any): void; }) & { ...; }; }' is not assignable to type '{ create: LooseRuleCreateFunction; meta?: object | undefined; }'.
            The types returned by 'create(...)' are incompatible between these types.
              Type '(false | { ImportDeclaration(node: any): void; VariableDeclaration(node: any): void; }) & { Literal(node: any): void; JSXAttribute(node: any): void; JSXText(node: any): void; TemplateLiteral(node: any): void; }' is not assignable to type 'Record<string, Function | undefined>'.
                Type 'false & { Literal(node: any): void; JSXAttribute(node: any): void; JSXText(node: any): void; TemplateLiteral(node: any): void; }' is not assignable to type 'Record<string, Function | undefined>'.
                  Index signature for type 'string' is missing in type 'Boolean & { Literal(node: any): void; JSXAttribute(node: any): void; JSXText(node: any): void; TemplateLiteral(node: any): void; }'

damisparks avatar Oct 02 '24 09:10 damisparks

Workaround

Use JSDoc-style type assertion to fix the types:

eslint.config.js

import react from 'eslint-plugin-react';

export const config = [
  {
    plugins: {
      // Type assertion is workaround for incorrect TypeScript
      // types in eslint-plugin-react
      //
      // TODO: Remove when types are fixed in eslint-plugin-react
      // - https://github.com/jsx-eslint/eslint-plugin-react/issues/3838
      react: /** @type {import('eslint').ESLint.Plugin} */ (react),

Or, if you use eslint.config.ts (ESLint 9.9.0+ experimental feature), use a TypeScript type assertion:

eslint.config.ts

import type { ESLint } from 'eslint';
import react from 'eslint-plugin-react';

export const config = [
  {
    plugins: {
      // Type assertion is workaround for incorrect TypeScript
      // types in eslint-plugin-react
      //
      // TODO: Remove when types are fixed in eslint-plugin-react
      // - https://github.com/jsx-eslint/eslint-plugin-react/issues/3838
      react: react as ESLint.Plugin,

karlhorky avatar Oct 06 '24 15:10 karlhorky

@ocavue @ljharb thanks for the PR, review and merge in #3840 🙌

I guess this will be released in a new patch or minor version?

karlhorky avatar Nov 27 '24 08:11 karlhorky

When is the next version scheduled?

carlos-lopez-SaRa avatar Dec 14 '24 02:12 carlos-lopez-SaRa

Versions aren’t scheduled, they just happen when i have time.

ljharb avatar Dec 14 '24 16:12 ljharb

So no idea when I guess? personally I wouldn't close an issue until the fix has been deployed to prod, npm in this case...

carlos-lopez-SaRa avatar Dec 15 '24 22:12 carlos-lopez-SaRa

@carlos-lopez-SaRa that's not how github works; every project closes issues when it's merged, not when it's released.

ljharb avatar Dec 16 '24 00:12 ljharb

that's not how github works; every project closes issues when it's merged, not when it's released

I guess this is getting to be a bit of a tangent (and probably also somewhat controversial), but IMO that shouldn't be how open source projects set up GitHub.

There are plenty of open source projects where issue closure is accompanied by an almost instant publish of a version, often with automatic communication in the issue and PR. This is in most circumstances automated, but sometimes also done manually by maintainers as part of the merge process.

Would be so amazing if more projects eliminated or reduced the limbo state of "merged but not released".

My reasoning for why this is good:

  1. Reduces open questions and wondering from the community about whether the change was really made
  2. Reduces unnecessary interaction from the community about the change (potentially new issues and PRs)

karlhorky avatar Dec 16 '24 05:12 karlhorky

@karlhorky "should" is irrelevant. what "is" is that the vast majority of projects do not publish from CI (which requires a single factor btw, and is thus much less secure), and thus all projects are in this state for some time.

ljharb avatar Dec 16 '24 06:12 ljharb

@carlos-lopez-SaRa that's not how github works; every project closes issues when it's merged, not when it's released.

The reason it's closed on merge is that the PR noted in the description it closes this issue, it is how Github works, whether should or not, we have little control over it.

Most popular & well maintained open source projects I've seen will reduce friction by either providing an immediate deployment, or a timeline for it, not saying this repo should do it, just noting my own experience.

carlos-lopez-SaRa avatar Dec 16 '24 06:12 carlos-lopez-SaRa