Skip to content

linter: no-unnecessary-type-assertion false positive on generic functions with typeAware enabled #20656

@EurFelux

Description

@EurFelux

Description

When "options": { "typeAware": true } is enabled, typescript/no-unnecessary-type-assertion incorrectly flags type assertions on generic function calls (notably querySelector) as unnecessary.

The rule resolves the generic type parameter from the as assertion target instead of using the declared default, causing it to conclude the expression "already has" the asserted type.

Minimal Reproduction

Repo: https://github.com/EurFelux/oxlint-type-assertion-bug

git clone https://github.com/EurFelux/oxlint-type-assertion-bug
cd oxlint-type-assertion-bug
npm install
npx oxlint --tsconfig tsconfig.json repro.ts

repro.ts:

// querySelector<E extends Element = Element>(selectors: string): E | null
// Default E = Element, so return type should be Element | null

// BUG: oxlint says "This expression already has the type 'HTMLCanvasElement | null'"
// Expected: no error (this assertion narrows Element | null → HTMLCanvasElement | null)
export const a = document.querySelector('.foo') as HTMLCanvasElement | null

.oxlintrc.json:

{
  "options": { "typeAware": true },
  "rules": { "typescript/no-unnecessary-type-assertion": "error" }
}

Expected Behavior

document.querySelector('.foo') returns Element | null (generic E defaults to Element). Asserting to HTMLCanvasElement | null is a legitimate narrowing and should not be flagged.

Actual Behavior

  x typescript-eslint(no-unnecessary-type-assertion): This assertion is unnecessary since it does not change the type of the expression.
   ,-[repro.ts:1:49]
 1 | export const a = document.querySelector('.foo') as HTMLCanvasElement | null
   :                  ... This expression already has the type 'HTMLCanvasElement | null'
   `----

Root Cause

querySelector has overloads:

querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;
querySelector<K extends keyof SVGElementTagNameMap>(selectors: K): SVGElementTagNameMap[K] | null;
querySelector<E extends Element = Element>(selectors: string): E | null;

For querySelector('.foo'), only the third overload matches. The default for E is Element, so the return type should be Element | null.

However, tsgolint appears to infer E from the as assertion target (e.g., E = HTMLCanvasElement), then concludes the assertion is a no-op. The assertion context should not influence the generic parameter resolution for the pre-assertion expression type.

Impact

  • Using --fix silently removes necessary type assertions, causing TypeScript compilation errors
  • After removal, querySelector('.foo') returns Element, losing access to .style, .focus(), .value, etc.
  • Affects any generic function with a default type parameter, not just querySelector

Environment

  • oxlint: 1.56.0
  • oxlint-tsgolint: latest
  • TypeScript: 5.8.3
  • OS: macOS Darwin 25.3.0
  • Requires: "options": { "typeAware": true }

Without typeAware: true, the rule works correctly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions