Search code examples
typescripteslintindentationtypescript-eslint

Typescript ESLint Indent rule doesn't indent multi-line object parameters correctly


I have a code block that looks like this:

this.$api
  .get<AxiosResponse<Resource>>('/resources/', {
    params: {
      pageNumber: 1,
      pageLength: 10,
    }
  });

My typescript ESLint is autoformatting this codeblock like so:

this.$api
  .get<AxiosResponse<Resource>>('/resources/', {
  params: {
    pageNumber: 1,
    pageLength: 10,
  }
});

This is not ideal and is imo very confusing. Similar behavior is referenced in an issue for @typescript-eslint here.

How can I update my ESLint config not to allow this?

Current ESLint config:

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript/recommended',
  ],
  env: {
    'vue/setup-compiler-macros': true,
  },
  overrides: [
    {
      files: ['cypress/integration/**.spec.{js,ts,jsx,tsx}'],
      extends: ['plugin:cypress/recommended'],
    },
  ],
  rules: {
    quotes: [2, 'single', { avoidEscape: true }],
    indent: 'off',
    '@typescript-eslint/no-unused-vars': 'off',
    'vue/max-attributes-per-line': [
      'error',
      {
        singleline: {
          max: 1,
        },
        multiline: {
          max: 1,
        },
      },
    ],
    'vue/html-indent': [
      'error',
      2,
      {
        attribute: 1,
        baseIndent: 1,
        closeBracket: 0,
        alignAttributesVertically: false,
        ignores: [],
      },
    ],
    'vue/html-closing-bracket-newline': [
      'error',
      {
        singleline: 'never',
        multiline: 'always',
      },
    ],
    'vue/script-indent': [
      'error',
      2,
      {
        baseIndent: 0,
        switchCase: 1,
        ignores: [],
      },
    ],
    'vue/attributes-order': [
      'error',
      {
        order: [
          'DEFINITION',
          'LIST_RENDERING',
          'CONDITIONALS',
          'RENDER_MODIFIERS',
          'GLOBAL',
          ['UNIQUE', 'SLOT'],
          'TWO_WAY_BINDING',
          'OTHER_DIRECTIVES',
          'OTHER_ATTR',
          'EVENTS',
          'CONTENT'
        ],
        'alphabetical': false
      }
    ],
    '@typescript-eslint/indent': [
      'error',
      2,
      {
        SwitchCase: 1,
        ObjectExpression: 1,
        MemberExpression: 1,
        CallExpression: {
          arguments: 1,
        },
      }
    ]
  },
};


Solution

  • So I actually discovered the answer to this but none of the relevant threads I found had an answer and they were also all closed issues so I couldn't leave any comments.

    The solution is to tell Typescript ESLint to ignore the arguments in a call expression. Once I figured out this trick I also told it to ignore several other node types using the ignoredNodes property in the configuration for the @typescript-eslint/indent rule.

    Bear in mind that it is totally critical that you also disable the default indent rule.

    /* eslint-env node */
    require('@rushstack/eslint-patch/modern-module-resolution');
    
    module.exports = {
      root: true,
      extends: [
        'plugin:vue/vue3-essential',
        'eslint:recommended',
        '@vue/eslint-config-typescript/recommended',
      ],
      env: {
        'vue/setup-compiler-macros': true,
      },
      overrides: [
        {
          files: ['cypress/integration/**.spec.{js,ts,jsx,tsx}'],
          extends: ['plugin:cypress/recommended'],
        },
      ],
      rules: {
        quotes: [2, 'single', { avoidEscape: true }],
        indent: 'off',                                  // !!!! VERY IMPORTANT !!!!
        '@typescript-eslint/no-unused-vars': 'off',
        'vue/max-attributes-per-line': [
          'error',
          {
            singleline: {
              max: 1,
            },
            multiline: {
              max: 1,
            },
          },
        ],
        'vue/html-indent': [
          'error',
          2,
          {
            attribute: 1,
            baseIndent: 1,
            closeBracket: 0,
            alignAttributesVertically: false,
            ignores: [],
          },
        ],
        'vue/html-closing-bracket-newline': [
          'error',
          {
            singleline: 'never',
            multiline: 'always',
          },
        ],
        'vue/script-indent': [
          'error',
          2,
          {
            baseIndent: 0,
            switchCase: 1,
            ignores: [],
          },
        ],
        'vue/attributes-order': [
          'error',
          {
            order: [
              'DEFINITION',
              'LIST_RENDERING',
              'CONDITIONALS',
              'RENDER_MODIFIERS',
              'GLOBAL',
              ['UNIQUE', 'SLOT'],
              'TWO_WAY_BINDING',
              'OTHER_DIRECTIVES',
              'OTHER_ATTR',
              'EVENTS',
              'CONTENT'
            ],
            'alphabetical': false
          }
        ],
        '@typescript-eslint/indent': [
          'error',
          2,
          {
            SwitchCase: 1,
            ObjectExpression: 1,
            MemberExpression: 1,
            CallExpression: {
              arguments: 1,
            },
            'ignoredNodes': [
              'PropertyDefinition[decorators]',
              'TSUnionType',
              'FunctionExpression[params]',
              'CallExpression[arguments]',              // !!!! VERY IMPORTANT !!!!
            ],
          }
        ]
      },
    };