DOM & Event Types
TypeScript's DOM types give every element and event a precise shape — from HTMLInputElement to MouseEvent — so querySelector , addEventListener , and event handlers are fully checked and autocompleted.
Learn DOM & Event Types in our free TypeScript course — an interactive lesson with runnable examples, a practice exercise and a quick reference.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
💡 Real-World Analogy
Think of the DOM as a building full of labelled rooms. querySelector is asking the receptionist for a room by name — they might hand you a key (the element) or shrug because it doesn't exist ( null ). Telling them "I want the input room" (the generic) means the key fits an input's locks specifically. Events are messages slipped under the door: a "click" note carries coordinates, a "keydown" note carries a key — same envelope ( Event ), different contents.
1. Element Types & querySelector Generics
Every DOM element has a precise type — HTMLInputElement , HTMLButtonElement , HTMLCanvasElement , all extending HTMLElement . querySelector takes a generic so you can say which one you expect, and it always returns YourType | null because the element may not be found.
2. Event vs MouseEvent vs KeyboardEvent
Events form a hierarchy : MouseEvent and KeyboardEvent extend the base Event , adding members like clientX or key . When you call addEventListener("click", ...) , TypeScript already knows the handler gets a MouseEvent — no casting needed.
3. Narrowing e.target & Typed Handlers
e.target is deliberately broad — EventTarget | null — because the event could have started on any descendant. To read element properties you narrow it with instanceof (or reach for the more specific e.currentTarget ). The handler-registration pattern itself is captured by a tiny typed event-emitter .
🎯 Your Turn
Narrow an event by its type field, just like you'd narrow a DOM event union. Fill in the two blanks marked ___ , then run it.
Common Errors (and the fix)
- ❌ "Object is possibly 'null'" — you used a querySelector result without checking. ✅ Guard with if (el) or use optional chaining el?.value .
- ❌ "Property 'value' does not exist on type 'Element'" — you didn't specify the element type. ✅ Use querySelector<HTMLInputElement> so .value is known.
- ❌ "Property 'clientX' does not exist on type 'Event'" — your handler is typed as plain Event . ✅ Use the correct event name so TS infers MouseEvent , or narrow.
- ❌ "Property 'value' does not exist on type 'EventTarget'" — e.target is too broad. ✅ if (e.target instanceof HTMLInputElement) before reading it.
Pro Tips
- 💡 Enable the "dom" lib in tsconfig.json ( compilerOptions.lib ) so all element/event types are available.
- 💡 Prefer instanceof over as for e.target — it's a real runtime check, not just a type assertion.
- 💡 Reach for e.currentTarget when you want the element the listener is attached to; it's easier to type than e.target .
- 💡 Use the HTMLElementTagNameMap behind the scenes: querySelector("input") (a tag selector) already infers HTMLInputElement automatically.
📚 Keep Learning
- 📖 TypeScript Handbook — DOM Manipulation
- 📖 Handbook — instanceof Narrowing
- 📖 tsconfig — the "dom" lib option
Frequently Asked Questions
Mini-Challenge: A Typed Handler Map
No blanks this time — just a brief and a starting outline. Build the dispatcher yourself, run it, and check your output against the example in the comments.
🎉 Lesson Complete
- ✅ querySelector<HTMLInputElement> steers the result to a specific element type
- ✅ Element lookups return YourType | null — always handle the null
- ✅ MouseEvent and KeyboardEvent extend Event with extra members
- ✅ addEventListener infers the right event subtype from the event name
- ✅ Narrow e.target ( EventTarget ) with instanceof before using element members
- ✅ Next lesson: Generic Defaults & Inference
Practice quiz
How do you tell TypeScript the exact element type from querySelector?
- Cast with 'as any'
- Use getElementById
- Use the generic: document.querySelector<HTMLInputElement>("#email")
- It is automatic for every selector
Answer: Use the generic: document.querySelector<HTMLInputElement>("#email"). The generic overload steers the result type, e.g. querySelector<HTMLInputElement>.
What is the result type of 'document.querySelector<HTMLInputElement>("#email")'?
- HTMLInputElement | null
- HTMLInputElement
- Element
- HTMLElement
Answer: HTMLInputElement | null. querySelector returns YourType | null because the element might not be found.
Why is querySelector's result possibly null?
- A bug in the DOM lib
- Because of strict mode only
- It is never null
- The selector might not match any element
Answer: The selector might not match any element. TypeScript types it as YourType | null to force you to handle the not-found case.
What is the return type of document.getElementById("box")?
- HTMLDivElement
- HTMLElement | null
- Element
- any
Answer: HTMLElement | null. getElementById has no generic and returns HTMLElement | null.
Which relationship is correct for event types?
- MouseEvent and KeyboardEvent extend Event
- Event extends MouseEvent
- They are unrelated
- KeyboardEvent extends MouseEvent
Answer: MouseEvent and KeyboardEvent extend Event. MouseEvent and KeyboardEvent both extend the base Event with extra members.
Inside addEventListener("click", handler), what type is the handler's event?
- Event
- KeyboardEvent
- MouseEvent
- any
Answer: MouseEvent. TypeScript infers the correct subtype (MouseEvent) from the known event name 'click'.
Which property does a KeyboardEvent have that the base Event does not?
- clientX
- key
- preventDefault
- target
Answer: key. KeyboardEvent adds members like 'key' and 'code'; clientX belongs to MouseEvent.
What is the type of e.target?
- HTMLElement
- Element
- the specific element always
- EventTarget | null
Answer: EventTarget | null. e.target is EventTarget | null, deliberately broad, so you must narrow it before use.
How do you safely read element-only members off e.target?
- Cast with 'as HTMLInputElement'
- Narrow with 'if (e.target instanceof HTMLInputElement)'
- Ignore the type
- Use e.target.value directly
Answer: Narrow with 'if (e.target instanceof HTMLInputElement)'. instanceof both checks and narrows, far safer than an 'as' cast.
Why does e.currentTarget often not need narrowing while e.target does?
- currentTarget is always null
- target is faster
- currentTarget is the element the listener is attached to, so it can be typed directly
- They are identical
Answer: currentTarget is the element the listener is attached to, so it can be typed directly. currentTarget is the element the listener is on and can be typed; target could be any descendant.
Continue this course
- Previous: Typing Promises & async
- Next: Generic Defaults & Inference