Skip to content

Bug: [no-unnecessary-type-assertion] autofix produces invalid code for TSTypeAssertion of ObjectExpression at left edge of an ExpressionStatement #12418

Description

@Deftera186

Before You File a Bug Report Please Confirm You Have Done The Following...

  • I have tried restarting my IDE and the issue persists.
  • I have updated to the latest version of the packages.
  • I have searched for related issues and found none that matched my issue.
  • I have read the FAQ and my problem is not listed.

Playground Link

https://typescript-eslint.io/play/#ts=5.7.2&fileType=.ts&code=DwbwBAhgXGB2CuBbARgUwE5gL4D5zTAEZsA6ADwG4AoUSGBFDbPOo7MAaiOtoIbUy58MYlgA0YAGYB7aQAoAlNSA&eslintrc=N4KABGBEBOCuA2BTAzpAXGUEKQAIBcBPABxQGNoBLY-AWhXkoDt8B6Jge1tiacTJTIAhtEK0ipWkOTJE0fJQ5N0UOdA7RI4MAF8QOoA&tsconfig=N4KABGBEDGD2C2AHAlgGwKYCcDyiAuysAdgM6QBcYoEEkJemy0eAcgK6qoDCAFutAGsylBm3TgwAXxCSgA&tokens=false

Repro Code

<{ a: number }>{ a: 1 }.x;
<{ a: number }>{ a: 1 } + 1;
<{ a: number }>{ a: 1 }, foo();

ESLint Config

module.exports = {
  parser: "@typescript-eslint/parser",
  rules: {
    "@typescript-eslint/no-unnecessary-type-assertion": "error",
  },
};

tsconfig

{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

Expected Result

Autofix produces parseable code that preserves the original semantics, e.g. wraps the object literal in parentheses:

({ a: 1 }).x;
({ a: 1 }) + 1;
({ a: 1 }), foo();

Actual Result

The fixer removes the <...> tokens unconditionally, leaving the object literal at the start of the statement, where { is parsed as a block:

{ a: 1 }.x;       // SyntaxError on `.x`
{ a: 1 } + 1;     // parses as Block + leading-`+` expression statement
{ a: 1 }, foo();  // parses as Block + sequence-expression statement

Additional Info

Follow-up to #12393 / #12394.

#12394 covers two shapes: arrow concise body (() => <T>{ ... }) and a bare ExpressionStatement whose entire expression is the assertion (<T>{ ... };). It does not cover the broader class where the assertion sits at the left edge of a larger expression that is itself an ExpressionStatement. The leading { after the fix still leads the statement and gets parsed as a block.

Affected positions (left-edge descent from the ExpressionStatement's expression):

  • MemberExpression.object<T>{a:1}.x;
  • CallExpression.callee<T>{a:1}();
  • NewExpression.calleenew <T>{a:1}(); (unusual, but parses)
  • BinaryExpression.left<T>{a:1} + 1;
  • LogicalExpression.left<T>{a:1} && foo;
  • ConditionalExpression.test<T>{a:1} ? a : b;
  • SequenceExpression.expressions[0]<T>{a:1}, foo();
  • TaggedTemplateExpression.tag<T>{a:1}`x`; (also unusual)
  • AssignmentExpression.left is invalid here, so n/a.

Catching this properly means walking up through left-edge parents until the parent is the ExpressionStatement, then wrapping in parens — analogous to how a pretty-printer decides "starts-with-{" needs grouping.

Discussed on #12394 (review thread r3347832383); the maintainer reviewing #12394 was fine with deferring the broader class to a separate issue. Filing so it isn't lost. Severity is low — these shapes are uncommon in real code, but the autofix is silently incorrect when they occur (some variants change semantics rather than throw, which is arguably worse).

AI disclosure: Used an AI assistant to help enumerate the left-edge positions and format the issue. The bug class itself was identified during review of #12394.

💖

Metadata

Metadata

Assignees

No one assigned

    Labels

    accepting prsGo ahead, send a pull request that resolves this issuebugSomething isn't workingpackage: eslint-pluginIssues related to @typescript-eslint/eslint-plugin

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions