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.
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 ofElement
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 anElement
. - ❌ Not every
Element
is anHTMLElement
.
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:
1. Use querySelectorAll with a generic (recommended)
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 returnsElement
, 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
- Use the generic overload
👉 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>
orReact.KeyboardEvent<HTMLButtonElement>
for correct inference.