Why does querySelectorAll in TypeScript return Element instead of HTMLElement?

Why does querySelectorAll in TypeScript return Element instead of HTMLElement

Understand why querySelectorAll in TypeScript is typed as Element, how it relates to the DOM spec, and the right way to work with HTMLElement arrays in your code.

Table of Contents

Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


If you’ve written code like this in TypeScript:

const items = document.querySelectorAll('.dropdown-item')
items[0].innerText // ❌ TS Error: Property 'innerText' does not exist on type 'Element'

…you’ve likely wondered:

Why does TypeScript think this is just an Element, when I know it’s an HTML node like <div> or <li>?

The answer comes from the DOM specification and how TypeScript models it.

Element vs HTMLElement

The DOM defines a hierarchy of nodes. Two important ones are:

  • Element A base type for any element in the DOM: HTML, SVG, or MathML. It does not guarantee HTML-specific properties like .innerText or .style.

  • HTMLElement A subtype of Element for HTML elements. It exposes HTML-only APIs such as .innerText, .style, .tabIndex.

Hierarchy overview:

Node
 └─ Element
     ├─ HTMLElement
     │    ├─ HTMLDivElement
     │    ├─ HTMLInputElement
     │    └─ ...
     └─ SVGElement

So:

  • ✅ Every HTMLElement is an Element.
  • ❌ Not every Element is an HTMLElement.

What does querySelectorAll return?

The DOM spec defines:

querySelectorAll(selectors: string): NodeListOf<Element>

This is deliberate: the browser doesn’t know whether your selector matches HTML, SVG, or MathML nodes. TypeScript reflects that by returning NodeListOf<Element>.

const nodes = document.querySelectorAll('.dropdown-item')
// nodes: NodeListOf<Element>

Even if you’re sure the selector only matches HTML elements, TypeScript won’t assume that.

Array.from vs Spread syntax

When turning a NodeList into an array, you may notice subtle typing differences:

const items1 = Array.from(document.querySelectorAll('.dropdown-item'))
// items1: Element[]

const items2 = [...document.querySelectorAll('.dropdown-item')]
// items2: Element[]

Both produce Element[]. However, Array.from can take a contextual type:

const items: HTMLElement[] = Array.from(
  document.querySelectorAll('.dropdown-item')
)
// ✅ items: HTMLElement[]

With spread [ ... ], TypeScript won’t infer from the target type — it keeps Element[].

Getting HTMLElement[]

To work with HTML-specific properties safely, you have a few options:

const items = document.querySelectorAll<HTMLElement>('.dropdown-item')
// items: NodeListOf<HTMLElement>

2. Cast the result

const items = [...document.querySelectorAll('.dropdown-item')] as HTMLElement[]

3. Use a runtime type guard

If your selector may match mixed elements:

const items = [...document.querySelectorAll('.dropdown-item')]
  .filter((el): el is HTMLElement => el instanceof HTMLElement)

Quick comparison

Code Type in TS Notes
document.querySelectorAll('.dropdown-item') NodeListOf<Element> Always general Element.
Array.from(document.querySelectorAll('.dropdown-item')) Element[] Converts to array.
const items: HTMLElement[] = Array.from(document.querySelectorAll('.dropdown-item')) HTMLElement[] Context narrows to HTML.
document.querySelectorAll<HTMLElement>('.dropdown-item') NodeListOf<HTMLElement> ✅ Cleanest option.
[...document.querySelectorAll('.dropdown-item')] Element[] Spread keeps general type.
[...document.querySelectorAll('.dropdown-item')] as HTMLElement[] HTMLElement[] Manual cast.
[...document.querySelectorAll('.dropdown-item')].filter((el): el is HTMLElement => el instanceof HTMLElement) HTMLElement[] Safe runtime check.

React example

If you’re working in React, the same type principles apply when handling DOM events. For example, typing an event handler on an <input>:

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  console.log(event.target.value) // ✅ typed as string
}

Or for a button key handler:

const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
  if (event.key === 'Enter') {
    // handle action
  }
}

This way, you don’t need casts — React’s synthetic events provide the correct HTMLElement subtype.

Conclusion

TypeScript is conservative by design:

  • querySelectorAll always returns Element, because your selector might not be limited to HTML.

  • If you know it’s HTML, you can:

    • Use the generic overload querySelectorAll<HTMLElement>()
    • Cast to HTMLElement[]
    • Or add a runtime guard for mixed cases

👉 The generic overload is usually the cleanest solution:

document.querySelectorAll<HTMLElement>('.dropdown-item')

It keeps your code type-safe, readable, and avoids unnecessary casting.

Key takeaways

  • Element is generic; HTMLElement is specific.
  • TypeScript matches the DOM spec strictly.
  • Use generics, casts, or guards to work safely with HTML collections.
  • In React, type events with React.ChangeEvent<HTMLInputElement> or React.KeyboardEvent<HTMLButtonElement> for correct inference.

About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.