Typing events
See how TypeScript types each event from its name, why event.target is only an EventTarget, how to type a handler, and how to type CustomEvent detail.
- 01Read an event's typeSay which event type a listener gets, like KeyboardEvent or PointerEvent, and where TypeScript looks it up.
- 02Use target and currentTarget safelyExplain why both are typed EventTarget | null, and get a real element type with a variable or instanceof.
- 03Type handlers and custom eventsAnnotate a handler written on its own, and give a CustomEvent's detail a type that listeners can see.
The event name picks the event type
A search box on a shop page reacts to key presses. In JavaScript you write a listener with addEventListener, and the browser hands it an event object: facts about what happened, like which key was pressed (the Introduction to events lesson covers this). In TypeScript, that event object has a type, and you usually don't write it:
const searchBox = document.createElement("input"); searchBox.addEventListener("keydown", (event) => { // ^? console.log(`You pressed ${event.key}`);}); searchBox.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));Line 1 makes an <input> text box. Line 3 listens for "keydown", the event a key press sends. The answer after the ^? on line 4 shows the type TypeScript gave event: KeyboardEvent. Line 5 reads event.key, the key that was pressed, and every KeyboardEvent has one. Line 8 sends a made-up key press with dispatchEvent, so the listener runs without a real keyboard, and the code prints You pressed Enter.
You never wrote KeyboardEvent. TypeScript worked it out from the name "keydown". Change the name, and the type changes with it:
const addButton = document.createElement("button"); addButton.addEventListener("click", (event) => { console.log(`You pressed ${event.key}`);}); addButton.click();Line 3 listens for "click" on a button. A click is a PointerEvent, and line 4 reads event.key. TypeScript marks it, on purpose here: Property 'key' does not exist on type 'PointerEvent' means "this kind of event has no key". Plain JavaScript would run it and print You pressed undefined when line 7 clicks the button.
Try it yourself: type a different event name on line 2, and watch the answer on line 6 and the list under the code.
const searchBox = document.createElement("input");const eventName = "keydown"; searchBox.addEventListener(eventName, (event) => { console.log(`You pressed ${event.key}`); // ^?}); searchBox.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));keydownKeyboardEventNo errorsprints You pressed Enter"keydown" is a key press, so event is a KeyboardEvent, and a KeyboardEvent has key. No errors, and line 9's pretend key press prints You pressed Enter.
With keyup, event is still a KeyboardEvent, so there are no errors. click gives a PointerEvent and focus a FocusEvent, and neither has a key. The typo keydwn is the surprise: TypeScript doesn't mark the name. It gives event the plain type Event, which every event shares, and the error shows up on line 5 instead.
addEventListener and gives event the type it finds. A name that isn't in the list, like "keydwn", gets plain Event.A parcel arrives with a printed label: "shoes". Before you open it, you already know what's inside and that you'll need to try them on, not plug them in. If the label is smudged, you only know it's a parcel, and you have to be careful with it.
- In real life: The label on a parcel says "shoes"
- In TypeScript: The name you pass,
"keydown" - In real life: You know it holds shoes before you open it
- In TypeScript: TypeScript knows
eventis aKeyboardEventbefore anything runs - In real life: The post office's list says which label means which contents
- In TypeScript: TypeScript's list pairs each event name with its type
- In real life: A parcel with a smudged label could hold anything
- In TypeScript: A name TypeScript doesn't know gives a plain
Event
The event's name picks the event's type. TypeScript looks up the name you pass to addEventListener, like "keydown", and gives the listener's event the type it finds, like KeyboardEvent. Then it checks everything you read from the event against that type.
How addEventListener knows the type
TypeScript knows the browser through declaration files: files that only describe types, with no code to run. The browser's are in a file called lib.dom.d.ts. Here is the part that matters for events, shortened:
interface GlobalEventHandlersEventMap {
"click": PointerEvent;
"focus": FocusEvent;
"input": Event;
"keydown": KeyboardEvent;
"submit": SubmitEvent;
// … and about 100 more
}
interface HTMLElementEventMap extends ElementEventMap, GlobalEventHandlersEventMap {}
interface HTMLInputElement {
addEventListener<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLInputElement, ev: HTMLElementEventMap[K]) => any,
): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject): void;
}In plain words, line by line:
- Lines 1 to 8 are an interface, a list of names and types. Each line pairs an event name with the type of event it sends:
"keydown"sends aKeyboardEvent. The real list has more than 100 names. - Line 10 makes
HTMLElementEventMap, the list for elements, out of that list and a short one,ElementEventMap.windowanddocumenthave lists of their own,WindowEventMapandDocumentEventMap, built the same way. - Lines 13 to 16 are the first version of
addEventListener.Kis a type parameter: it stands for the name you pass, like"keydown", and must be one of the list's names (the Generic functions lesson explains type parameters). The listener's event isHTMLElementEventMap[K]: the type written next to that name. - Line 17 is a second version that takes any text as the name. A name that isn't in the list lands here, and its listener gets a plain
Event. That's whykeydwnwasn't marked.
It's the same idea that makes document.createElement("input") give an HTMLInputElement: a list pairs each name with a type, and the method looks the name up. So you can look a type up yourself, with the event's name in square brackets. That's handy when you need to write the type down:
type KeyPress = HTMLElementEventMap["keydown"];// ^?type Click = HTMLElementEventMap["click"];// ^?type Typing = HTMLElementEventMap["input"];// ^?type Send = HTMLElementEventMap["submit"];// ^?Each line gives a name to the type found next to an event name. The answers show "keydown" is a KeyboardEvent (line 2), "click" a PointerEvent (line 4), and "submit" a SubmitEvent (line 8). Line 6 may surprise you: TypeScript lists "input", the event a text box sends as you type, as a plain Event. A text box sends an InputEvent, but a <select> or a checkbox sends a plain Event with the same name, so TypeScript only promises what they all have. You read the new text from the box itself, not from the event.
A PointerEvent is a kind of MouseEvent: it has everything a MouseEvent has, plus facts about the pointer, like whether it was a mouse, a pen, or a finger:
const addButton = document.createElement("button"); addButton.addEventListener("click", (event) => { console.log(event.clientX, event.shiftKey, event.pointerType);}); addButton.dispatchEvent(new PointerEvent("click", { clientX: 40, shiftKey: true, pointerType: "mouse" }));Line 4 reads clientX (how far from the left of the window the click was) and shiftKey (whether Shift was held), which come from MouseEvent, and pointerType, which PointerEvent adds. Line 7 sends a made-up click with those facts, and the code prints 40 true mouse.
The table below lists the events you'll use most. Read each row as: this name, the type the listener gets, and what that type adds.
| Event name | The type the listener gets | Properties you'll use |
|---|---|---|
"click" | PointerEvent, a kind of MouseEvent | clientX and clientY (where), button, shiftKey, and pointerType. |
"keydown" and "keyup" | KeyboardEvent | key (like "Enter"), code, shiftKey, and repeat. |
"focus" and "blur" | FocusEvent | relatedTarget: the element focus came from or is going to. |
"submit" | SubmitEvent | submitter: the button that sent the form. preventDefault() stops the page from reloading. |
"input" and "change" | Event | Nothing extra. Read the new text from the element itself. |
"beforeinput" | InputEvent | data (the text about to be typed) and inputType. |
| A name TypeScript doesn't know | Event | Only what every event has, like type, target, and preventDefault(). |
target and currentTarget
Every event has two properties that point at elements. event.target is where the event began. event.currentTarget is the element whose listener is running. They're different when an event bubbles: a click on an element inside also reaches every element around it (the Bubbling & capturing lesson covers it). Here a list holds a button, and the button holds a span:
const list = document.createElement("ul");const addButton = document.createElement("button");const label = document.createElement("span");label.textContent = "Add tea";addButton.append(label);list.append(addButton); list.addEventListener("click", (event) => { console.log(event.target === label); // ^? console.log(event.currentTarget === list); // ^?}); label.click();Lines 1 to 6 build the list, the button, and the span with the words Add tea. Line 8 puts the listener on the list. Line 15 clicks the span. The click begins at the span, so line 9 prints true: the target is the span. The listener is on the list, so line 11 prints true too: the current target is the list.
Now look at the answers on lines 10 and 12. TypeScript gives both the same type, EventTarget | null. An EventTarget is anything that can receive events: an element, but also window or a plain object made with new EventTarget(). TypeScript can't know where a click will begin, so it can't promise a span or a button. The | null covers an event that hasn't been sent yet, whose target is still null.
Add tea. The click begins at the span, so event.target is the span. The listener is on the list, so event.currentTarget is the list. TypeScript gives both the same type, EventTarget | null.That type gets in the way when you want something only an element has, like a text box's value. Try four ways of reading what the shopper typed:
const searchBox = document.createElement("input"); searchBox.addEventListener("input", (event) => { console.log(`Searching for ${event.target.value}`);}); searchBox.value = "green tea";searchBox.dispatchEvent(new Event("input"));Searching for green teaevent.target2 errors: lines 4 and 4Line 4 gets two errors. 'event.target' is possibly 'null': TypeScript allows for an event with no target. And Property 'value' does not exist on type 'EventTarget': TypeScript only knows the target is something that can receive events, not that it's a text box. The code still prints Searching for green tea when it runs.
event.target gets two errors on line 4, on purpose: it might be null, and an EventTarget has no value. event.currentTarget gets the same two, even though it's always searchBox here: TypeScript's browser types give every event the same currentTarget type. The two fixes work: use the searchBox variable, which is already an HTMLInputElement, or check the target with instanceof (the in & instanceof narrowing lesson explains how the check narrows the type). The code prints Searching for green tea every time: types never change what runs.
You might see event.target as HTMLInputElement in other code. That makes the errors go away without checking anything; the Type assertions lesson explains why it's a promise, not a check. The table below sums up the two properties. Read each row as a question, then the answer for each.
| Question | event.target | event.currentTarget |
|---|---|---|
| Which element is it? | Where the event began, like the span you clicked inside a button. | The element the listener is on, like the list. |
| Can it be a different element each time? | Yes. Any element inside can be where a click begins. | No. It's always the element you called addEventListener on. |
| What type does TypeScript give it? | EventTarget | null. | EventTarget | null too. |
| How do you get an element type? | Check it with instanceof, like event.target instanceof HTMLInputElement. | Use the variable you attached the listener to, like searchBox. |
The instanceof check matters most when one listener handles many elements, called event delegation. A menu has one listener, and it finds the button that was clicked:
const menu = document.createElement("ul");menu.innerHTML = `<li><button data-product="Tea"><span>Add tea</span></button></li>`; menu.addEventListener("click", (event) => { if (!(event.target instanceof Element)) return; const button = event.target.closest("button"); // ^? if (button) console.log(`Added ${button.dataset.product}`);}); menu.querySelector("span")?.click();Line 2 fills the menu with a button holding a span. Line 4 puts one listener on the menu. Line 5 stops unless the target is an Element; after it, TypeScript knows event.target is one. Line 6 calls closest("button"), which looks from the target outwards for a button. The answer on line 7 shows it gives HTMLButtonElement | null, so line 8 checks for null before it reads the button's data-product. Line 11 clicks the span, and the code prints Added Tea.
Handlers written on their own
So far every listener was written right inside addEventListener, and TypeScript filled in event from the call. That's called contextual typing (the Type annotations & inference lesson covers it). A handler written as a function of its own doesn't get that help:
function handleKey(event) { console.log(`You pressed ${event.key}`);} const searchBox = document.createElement("input");searchBox.addEventListener("keydown", handleKey);Line 1 declares handleKey, and line 6 attaches it. TypeScript checks a function where it's written, before it sees where it's used, so it marks line 1 on purpose: Parameter 'event' implicitly has an 'any' type. That means "nothing says what event is, so it would be any, which turns checking off". Strict mode reports it so you can write the type. Try four types for line 1:
function handleKey(event: KeyboardEvent) { console.log(`You pressed ${event.key}`);} const searchBox = document.createElement("input");searchBox.addEventListener("keydown", handleKey);searchBox.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));You pressed Enterevent: KeyboardEventNo errorsThe handler asks for a KeyboardEvent, and a keydown listener gets exactly that. No errors, and it prints You pressed Enter.
KeyboardEvent fits exactly. Event fits too, because every key press is an Event: a handler may ask for less than it gets. But then line 2 can't read key. MouseEvent asks for more than a key press has, so line 6 gets No overload matches this call. addEventListener has two versions, called overloads (the Function overloads lesson covers them), and the message means the call fits neither. The lines under it say which properties are missing, like clientX.
KeyboardEvent. A handler can listen when it asks for that type or for less, like plain Event. A handler that needs a MouseEvent can't: a key press has no clientX.To write the type without guessing, point at event in an inline listener, or look it up by name as in the last section. A function type can do the lookup once for every key handler:
type KeyHandler = (event: HTMLElementEventMap["keydown"]) => void; const logKey: KeyHandler = (event) => { console.log(`You pressed ${event.key}`); // ^?}; const searchBox = document.createElement("input");searchBox.addEventListener("keydown", logKey);searchBox.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));Line 1 says a KeyHandler is a function that takes the keydown event and returns nothing. Line 3 gives logKey that type, so its event needs no annotation: the answer on line 5 shows it's a KeyboardEvent. Line 9 attaches it, and line 10's pretend key press prints You pressed Escape.
Custom events and their detail
Your own code can send events too. A CustomEvent carries extra data in a property called detail (the Custom events lesson covers them). In TypeScript, CustomEvent is generic: you write the detail's type in angle brackets.
type CartAdd = { name: string; quantity: number }; const added = new CustomEvent<CartAdd>("cart:add", { detail: { name: "Tea", quantity: 2 },}); console.log(added.detail.name);// ^?Line 1 describes what a cart addition holds: a name and a quantity. Line 3 makes a CustomEvent<CartAdd> called "cart:add", and line 4 gives its detail. The answer on line 8 shows detail is a CartAdd, so line 7 prints Tea. TypeScript checks the detail you send against that type:
type CartAdd = { name: string; quantity: number }; const added = new CustomEvent<CartAdd>("cart:add", { detail: { name: "Tea", quantity: "2" },});Line 4 gives quantity the text "2", so TypeScript marks it on purpose: Type 'string' is not assignable to type 'number', meaning text where a number has to go.
CartAdd, is the type of detail. TypeScript checks the detail you send against it, and a listener that knows the event's type reads detail.quantity as a number.Sending is typed. Listening is harder, because the listener only has the name, and "cart:add" isn't in TypeScript's list:
const cartBadge = document.createElement("span"); cartBadge.addEventListener("cart:add", (event) => { console.log(`Added ${event.detail.quantity} ${event.detail.name}`);}); cartBadge.dispatchEvent(new CustomEvent("cart:add", { detail: { name: "Tea", quantity: 2 } }));Line 3 listens for "cart:add" on a cart badge, and line 7 sends one. The listener's event is a plain Event, so both reads on line 4 get Property 'detail' does not exist on type 'Event', on purpose. The code still prints Added 2 Tea. Checking event instanceof CustomEvent doesn't help much: it gives CustomEvent<any>, because instanceof can't look inside detail.
The fix is to add the name to the list. Interfaces with the same name join together, so you can add a line to HTMLElementEventMap (the Declaration merging lesson does this for window):
type CartAdd = { name: string; quantity: number }; declare global { interface HTMLElementEventMap { "cart:add": CustomEvent<CartAdd>; }} const cartBadge = document.createElement("span"); cartBadge.addEventListener("cart:add", (event) => { // ^? console.log(`Added ${event.detail.quantity} ${event.detail.name}`);}); cartBadge.dispatchEvent(new CustomEvent<CartAdd>("cart:add", { detail: { name: "Tea", quantity: 2 } }));Lines 3 to 7 add "cart:add" with the type CustomEvent<CartAdd>. declare global means "add this to the types every file sees". Now the answer on line 12 shows the listener's event is a CustomEvent<CartAdd>, line 13 has no errors, and the code prints Added 2 Tea.
declare global only works in a module: a file with an import or an export. This page treats every example as a module. In your own project, a file with neither gets the error Augmentations for the global scope can only be directly nested in external modules, so add export {}; at the end of it (Globals & module augmentation explains why).
The list types the listeners. It doesn't check the senders, because dispatchEvent accepts any event:
type CartAdd = { name: string; quantity: number }; declare global { interface HTMLElementEventMap { "cart:add": CustomEvent<CartAdd>; }} const cartBadge = document.createElement("span");cartBadge.addEventListener("cart:add", (event) => { console.log(`Added ${event.detail.quantity} ${event.detail.name}`);}); cartBadge.dispatchEvent(new CustomEvent("cart:add", { detail: { title: "Tea" } }));Line 14 sends a detail with a title and no name or quantity. TypeScript reports nothing, and the code prints Added undefined undefined. To check both sides, send and listen through two small functions:
type CartAdd = { name: string; quantity: number }; function sendCartAdd(target: EventTarget, detail: CartAdd) { target.dispatchEvent(new CustomEvent<CartAdd>("cart:add", { detail }));} function onCartAdd(target: EventTarget, handler: (detail: CartAdd) => void) { target.addEventListener("cart:add", (event) => { handler((event as CustomEvent<CartAdd>).detail); });} const cart = new EventTarget();onCartAdd(cart, (item) => console.log(`Added ${item.quantity} ${item.name}`));sendCartAdd(cart, { name: "Cake", quantity: 1 });Lines 3 to 5 make sendCartAdd, which only accepts a CartAdd. Lines 7 to 11 make onCartAdd, which hands its handler a CartAdd. Line 9 is the one place that says "this event is a CustomEvent<CartAdd>" with as. That promise holds as long as every "cart:add" event is sent through sendCartAdd. Line 13 makes a plain EventTarget to send them on, and the code prints Added 1 Cake.
The table below compares the three ways. Read each row as: this way, what it types, and what can still go wrong.
| The way | What gets typed | What can still go wrong |
|---|---|---|
Add the name to HTMLElementEventMap | Every element listener for that name gets CustomEvent<CartAdd>. | dispatchEvent isn't checked, so a sender can still send the wrong detail. |
Write sendCartAdd and onCartAdd helpers | The sender and the listener, through the helpers' parameters. | Code that calls dispatchEvent directly skips the helpers' check. |
Write event as CustomEvent<CartAdd> in each listener | Only that listener, and only because you said so. | Every listener repeats the promise, and nothing checks it. |
A search form with keyboard shortcuts
A real search form sends its text when you press Enter or the button, and clears when you press Escape. Its handlers are named functions, so each one says which event it takes:
const form = document.createElement("form");const searchBox = document.createElement("input");form.append(searchBox);document.body.append(form); function handleKey(event: KeyboardEvent) { if (event.key === "Escape") { searchBox.value = ""; console.log("Search cleared"); }} function handleSubmit(event: SubmitEvent) { event.preventDefault(); console.log(`Searching for ${searchBox.value}`);} searchBox.addEventListener("keydown", handleKey);form.addEventListener("submit", handleSubmit); searchBox.value = "green tea";form.requestSubmit();searchBox.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));Lines 1 to 4 build a form holding a search box and put it on the page. Lines 6 to 11 handle key presses: line 6 says the handler takes a KeyboardEvent, so line 7 can compare event.key with "Escape". Lines 13 to 16 handle sending the form: a SubmitEvent, and line 14's preventDefault() stops the page from reloading (Form events covers that). Lines 18 and 19 attach both, and TypeScript checks each handler against its event's name. Line 22 sends the form and line 23 presses Escape, so the code prints Searching for green tea, then Search cleared.
The handlers read searchBox directly, not event.target, so they need no checks. Now sort some one-line listeners by what TypeScript says about them:
document.addEventListener("keydown", (event) => event.key);document.addEventListener("click", (event) => event.key);document.addEventListener("click", (event) => event.clientX);document.addEventListener("input", (event) => event.target.value);document.addEventListener("submit", (event) => event.preventDefault());const show = (event: MouseEvent) => event.clientX; document.addEventListener("keydown", show);const show = (event: Event) => event.type; document.addEventListener("keydown", show);new CustomEvent<{ id: number }>("pick", { detail: { id: "7" } });
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
An event name works only when TypeScript can see the exact name. Kept in a let variable, it can't:
const searchBox = document.createElement("input");let eventName = "keydown";// ^? searchBox.addEventListener(eventName, (event) => { console.log(`You pressed ${event.key}`);});Line 2 stores the name with let, so it could change later, and the answer on line 3 shows its type is string: any text (the Literal types lesson explains why). Any text can't be looked up, so line 5's listener gets a plain Event, and line 6 is marked on purpose. With const, as in the playground, the type is exactly "keydown".
And a custom event's type doesn't make its detail appear:
type CartAdd = { name: string; quantity: number }; const added = new CustomEvent<CartAdd>("cart:add");console.log(added.detail);console.log(added.detail.name);Line 3 makes a CustomEvent<CartAdd> with no options. TypeScript has no error: the detail option is optional. When it runs, the browser sets detail to null, so line 4 prints null and line 5 throws a TypeError.
- "A misspelled event name is an error." No. Any text is allowed as a name. The listener gets a plain
Event, and an error appears only if you read something a plainEventlacks. - "event.currentTarget has the type of the element I listen on." No. It's
EventTarget | nullfor every event. Use the variable you attached the listener to. - "event.target as HTMLInputElement checks the target." No. It's a promise you make, and nothing checks it.
instanceofchecks it while the page runs. - "TypeScript types an input listener's event as an InputEvent." No. It lists
"input"as a plainEvent, because a<select>or a checkbox sends a plainEventwith that name."beforeinput"is the one TypeScript types as anInputEvent. - "A handler must take exactly the event's type." No. It may ask for less, like
Event, but never for more, like aMouseEventfor a key press. - "Adding my event to the list checks dispatchEvent." No. The list types listeners. Senders are checked only if they go through a typed function like
sendCartAdd. - "CustomEvent<CartAdd> always has a detail." No. Leave the option out, and
detailisnull, with no error.
Practice exercises
Each exercise is TypeScript. Press Edit & run on the starter code to try a fix: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.
What type does event get in this form listener? Guess first. Then point at event to check, or press Edit & run and add a // ^? line under it.
const form = document.createElement("form"); form.addEventListener("submit", (event) => { event.preventDefault();});const form = document.createElement("form"); form.addEventListener("submit", (event) => { // ^? event.preventDefault();});TypeScript's list pairs "submit" with SubmitEvent, so that's the type of event. The query on line 4 shows (parameter) event: SubmitEvent.
Two listeners read the same property. Which line will TypeScript mark?
const addButton = document.createElement("button");
const searchBox = document.createElement("input");
addButton.addEventListener("click", (event) => {
console.log(event.clientX);
});
searchBox.addEventListener("keydown", (event) => {
console.log(event.clientX);
});const addButton = document.createElement("button");const searchBox = document.createElement("input"); addButton.addEventListener("click", (event) => { console.log(event.clientX);}); searchBox.addEventListener("keydown", (event) => { console.log(event.key); // line 9: a key press has no clientX});Line 9 reads clientX from a KeyboardEvent, which has no clientX: Property 'clientX' does not exist on type 'KeyboardEvent'. Line 5 is fine, because a click has one. Reading event.key on line 9 instead fixes it.
TypeScript marks line 1. Give event a type that has clientX. What does the fixed code print?
function showPosition(event) { console.log(`Clicked at ${event.clientX}`);} const map = document.createElement("div");map.addEventListener("click", showPosition);map.dispatchEvent(new MouseEvent("click", { clientX: 40 }));function showPosition(event: MouseEvent) { console.log(`Clicked at ${event.clientX}`);} const map = document.createElement("div");map.addEventListener("click", showPosition);map.dispatchEvent(new MouseEvent("click", { clientX: 40 }));event: MouseEvent says what the handler needs, and a click gives at least that, so line 6 is fine. Line 7 sends a click 40 pixels from the left, and the code prints Clicked at 40.
Line 4 has two errors. Fix them without using as. What does your fixed code print?
const searchBox = document.createElement("input"); searchBox.addEventListener("input", (event) => { console.log(`Searching for ${event.target.value}`);}); searchBox.value = "cake";searchBox.dispatchEvent(new Event("input"));const searchBox = document.createElement("input"); searchBox.addEventListener("input", (event) => { if (event.target instanceof HTMLInputElement) { console.log(`Searching for ${event.target.value}`); }}); searchBox.value = "cake";searchBox.dispatchEvent(new Event("input"));Inside the instanceof check, TypeScript knows the target is an HTMLInputElement, so value is allowed. The box holds cake, so the code prints Searching for cake.
A menu sends a "filter:change" event. Make the listener know its detail, so line 6 has no error. What does the code print?
type Filter = { category: string }; const menu = document.createElement("div"); menu.addEventListener("filter:change", (event) => { console.log(`Showing ${event.detail.category}`);}); menu.dispatchEvent(new CustomEvent<Filter>("filter:change", { detail: { category: "cake" } }));type Filter = { category: string }; declare global { interface HTMLElementEventMap { "filter:change": CustomEvent<Filter>; }} const menu = document.createElement("div"); menu.addEventListener("filter:change", (event) => { console.log(`Showing ${event.detail.category}`);}); menu.dispatchEvent(new CustomEvent<Filter>("filter:change", { detail: { category: "cake" } }));With "filter:change" in HTMLElementEventMap, the listener's event is a CustomEvent<Filter>, so event.detail.category is a string. The menu sends cake, and the code prints Showing cake.
One listener on the menu finds the clicked button. Fix line 6's errors with checks, not as. What does the click on the second span print?
const menu = document.createElement("ul");menu.innerHTML = `<li><button data-product="Tea"><span>Add tea</span></button></li><li><button data-product="Cake"><span>Add cake</span></button></li>`; menu.addEventListener("click", (event) => { const button = event.target.closest("button"); console.log(`Added ${button.dataset.product}`);}); menu.querySelectorAll("span")[1].click();const menu = document.createElement("ul");menu.innerHTML = `<li><button data-product="Tea"><span>Add tea</span></button></li><li><button data-product="Cake"><span>Add cake</span></button></li>`; menu.addEventListener("click", (event) => { if (!(event.target instanceof Element)) return; const button = event.target.closest("button"); if (button) console.log(`Added ${button.dataset.product}`);}); menu.querySelectorAll("span")[1].click();After the instanceof Element check, closest("button") is allowed and gives HTMLButtonElement | null. The if (button) check handles a click outside any button. The click on the second span finds the Cake button, so it prints Added Cake.
Check your understanding
For each question, start from the event's name and look up its type. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7How does
addEventListenerknow that a"keydown"listener gets aKeyboardEvent?Choose an answer to see the explanation.
Question 2 of 7What type does
eventget in thisinputlistener?Read the code, then predictconst searchBox = document.createElement("input"); searchBox.addEventListener("input", (event) => { console.log(event.type); });Choose an answer to see the explanation.
Question 3 of 7What type does TypeScript give
whereinside this list listener?Read the code, then predictconst list = document.createElement("ul"); list.addEventListener("click", (event) => { const where = event.currentTarget; });Choose an answer to see the explanation.
Question 4 of 7Which line does TypeScript mark in this button code?
Read the code, then predictfunction showKey(event: KeyboardEvent) { console.log(event.key); } const addButton = document.createElement("button"); addButton.addEventListener("click", showKey);Choose an answer to see the explanation.
Question 5 of 7What does TypeScript say about this misspelled event name?
Read the code, then predictconst addButton = document.createElement("button"); addButton.addEventListener("clik", (event) => { console.log(event.clientX); });Choose an answer to see the explanation.
Question 6 of 7What does this custom event print, and does TypeScript complain?
Read the code, then predictconst added = new CustomEvent<{ count: number }>("cart:update"); console.log(added.detail);Choose an answer to see the explanation.
Question 7 of 7A handler typed
(event: Event) => voidis attached withaddEventListener("click", handler). What does TypeScript say?Choose an answer to see the explanation.
Key takeaways
- The name you pass to
addEventListenerpicks the event's type:"keydown"gives aKeyboardEvent,"click"aPointerEvent. - TypeScript looks the name up in lists like
HTMLElementEventMap. A name it doesn't know gives a plainEvent, with no error on the name. event.targetandevent.currentTargetare bothEventTarget | null. Use your element variable, or check withinstanceof.- A handler written on its own needs its event type written out. It may ask for less than the event gives, never more.
CustomEvent<T>types thedetailyou send. For listeners, add the name to the event list or use typed helpers.
Remember the one-liner.
The event's name picks its type, and the target is only an EventTarget until you check it.
Coming next: Typing fetch & JSON. Events come with types from the browser, but data from a server has none until you check it.