cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Read an event's typeSay which event type a listener gets, like KeyboardEvent or PointerEvent, and where TypeScript looks it up.
  • 02
    Use target and currentTarget safelyExplain why both are typed EventTarget | null, and get a real element type with a variable or instanceof.
  • 03
    Type 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:

A key listener on a search boxPop out in the code editor (opens in a new tab)TypeScript
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:

A click has no key (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

Playground: type an event name
A listener whose name you choosePop out in the code editor (opens in a new tab)TypeScript
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" }));
The listener listens forkeydown
What happens
event is aKeyboardEvent
TypeScriptNo errors
When it runsprints You pressed Enter
Try it yourself

"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.

The code is checked by the real TypeScript compiler each time you type. Only line 2 changes. Reset puts keydown back.

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.

Four event names, each with an arrow to the type TypeScript gives the event: "keydown" to KeyboardEvent, "click" to PointerEvent, "focus" to FocusEvent, and the misspelled "keydwn" to plain Event, with the note: not in the list.the name you passlooked upthe type of event"keydown"KeyboardEvent"click"PointerEvent"focus"FocusEvent"keydwn"Eventnot in the list
TypeScript looks up the name you pass to addEventListener and gives event the type it finds. A name that isn't in the list, like "keydwn", gets plain Event.
Real-life analogyA parcel with a label

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 event is a KeyboardEvent before 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 main idea

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:

The event list and addEventListener, shortened from lib.dom.d.tsText
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:

  1. 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 a KeyboardEvent. The real list has more than 100 names.
  2. Line 10 makes HTMLElementEventMap, the list for elements, out of that list and a short one, ElementEventMap. window and document have lists of their own, WindowEventMap and DocumentEventMap, built the same way.
  3. Lines 13 to 16 are the first version of addEventListener. K is 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 is HTMLElementEventMap[K]: the type written next to that name.
  4. 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 why keydwn wasn'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:

Look up an event's type by its namePop out in the code editor (opens in a new tab)TypeScript
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:

What a click listener can readPop out in the code editor (opens in a new tab)TypeScript
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.

Common event names and their types
Event nameThe type the listener getsProperties you'll use
"click"PointerEvent, a kind of MouseEventclientX and clientY (where), button, shiftKey, and pointerType.
"keydown" and "keyup"KeyboardEventkey (like "Enter"), code, shiftKey, and repeat.
"focus" and "blur"FocusEventrelatedTarget: the element focus came from or is going to.
"submit"SubmitEventsubmitter: the button that sent the form. preventDefault() stops the page from reloading.
"input" and "change"EventNothing extra. Read the new text from the element itself.
"beforeinput"InputEventdata (the text about to be typed) and inputType.
A name TypeScript doesn't knowEventOnly 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:

A click inside a listPop out in the code editor (opens in a new tab)TypeScript
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.

A list crate holds a button crate, which holds a card for the span Add tea. event.target points at the span, where the click began. event.currentTarget points at the list, where the listener is. A note says TypeScript types both as EventTarget | null.span: Add teabuttonlist (ul)event.targetwhere the click beganevent.currentTargetwhere the listener isTypeScript types both as EventTarget | null
You click the words 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:

Playground: read what the shopper typed
A search box that reports what you typePop out in the code editor (opens in a new tab)TypeScript
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"));
Console outputSearching for green tea
What happens
Line 4 readsevent.target
TypeScript2 errors: lines 4 and 4
Try it yourself

Line 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.

TypeScript checks the code for real, and the output is what the code prints. Each button changes only lines 3 to 5. Reset goes back to event.target.

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.

event.target and event.currentTarget, side by side
Questionevent.targetevent.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:

One listener finds the clicked buttonPop out in the code editor (opens in a new tab)TypeScript
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:

A handler with no type (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

Playground: what the handler asks for
A key handler written on its ownPop out in the code editor (opens in a new tab)TypeScript
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" }));
Console outputYou pressed Enter
What happens
Line 1 asks forevent: KeyboardEvent
TypeScriptNo errors
Try it yourself

The handler asks for a KeyboardEvent, and a keydown listener gets exactly that. No errors, and it prints You pressed Enter.

TypeScript checks the code for real. Each button changes only the parameter on line 1. Reset goes back to KeyboardEvent.

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.

A keydown sends a KeyboardEvent card. Arrows lead to three handlers. (event: KeyboardEvent) fits. (event: Event) asks for less and fits. (event: MouseEvent) needs clientX, which a key press doesn't have, so it's an error.a keydown sendsKeyboardEvent(event: KeyboardEvent)fits(event: Event)asks for less: fits(event: MouseEvent)needs clientX: error
A keydown sends a 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:

A handler type built from the event listPop out in the code editor (opens in a new tab)TypeScript
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.

A custom event with a typed detailPop out in the code editor (opens in a new tab)TypeScript
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:

A detail with the wrong type (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

A sender window makes new CustomEvent<CartAdd> with the detail name "Tea", quantity 2. An arrow carries it to a listener window that reads event.detail.quantity and gets 2. Notes say: detail must be a CartAdd, and quantity is a number if the listener knows the type.sendernew CustomEvent<CartAdd>name: "Tea", quantity: 2detail must be a CartAddlistenerevent.detail.quantity2a number, if the listener knows the type
The type in the angle brackets, 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:

A listener that doesn't know the event (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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):

Add the event to the element event listPop out in the code editor (opens in a new tab)TypeScript
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:

A wrong detail that nothing checksPop out in the code editor (opens in a new tab)TypeScript
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:

Helpers that type the sender and the listenerPop out in the code editor (opens in a new tab)TypeScript
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.

Three ways to type a custom event
The wayWhat gets typedWhat can still go wrong
Add the name to HTMLElementEventMapEvery 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 helpersThe sender and the listener, through the helpers' parameters.Code that calls dispatchEvent directly skips the helpers' check.
Write event as CustomEvent<CartAdd> in each listenerOnly 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:

A search form with typed handlersPop out in the code editor (opens in a new tab)TypeScript
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:

Error or fine?
  • 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" } });
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it. Each line is checked on its own.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

An event name works only when TypeScript can see the exact name. Kept in a let variable, it can't:

An event name in a let variable (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

A custom event made without a detailPop out in the code editor (opens in a new tab)TypeScript
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 plain Event lacks.
  • "event.currentTarget has the type of the element I listen on." No. It's EventTarget | null for 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. instanceof checks it while the page runs.
  • "TypeScript types an input listener's event as an InputEvent." No. It lists "input" as a plain Event, because a <select> or a checkbox sends a plain Event with that name. "beforeinput" is the one TypeScript types as an InputEvent.
  • "A handler must take exactly the event's type." No. It may ask for less, like Event, but never for more, like a MouseEvent for 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 detail is null, 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.

Exercise 1 · Warm-upName the event type

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.

Starter codePop out in the code editor (opens in a new tab)TypeScript
const form = document.createElement("form"); form.addEventListener("submit", (event) => {  event.preventDefault();});

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the line TypeScript marks

    Two listeners read the same property. Which line will TypeScript mark?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    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);
    });

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeType a handler written on its own

      TypeScript marks line 1. Give event a type that has clientX. What does the fixed code print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      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 }));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeRead the search box's text

        Line 4 has two errors. Fix them without using as. What does your fixed code print?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        const searchBox = document.createElement("input"); searchBox.addEventListener("input", (event) => {  console.log(`Searching for ${event.target.value}`);}); searchBox.value = "cake";searchBox.dispatchEvent(new Event("input"));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeType a custom filter event

          A menu sends a "filter:change" event. Make the listener know its detail, so line 6 has no error. What does the code print?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          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" } }));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeFind the clicked button

            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?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            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();

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              The Typing events quiz · 7 questionsScore: first tries count
              1. Question 1 of 7How does addEventListener know that a "keydown" listener gets a KeyboardEvent?

                Choose an answer to see the explanation.

              2. Question 2 of 7What type does event get in this input listener?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const searchBox = document.createElement("input");
                
                searchBox.addEventListener("input", (event) => {
                  console.log(event.type);
                });

                Choose an answer to see the explanation.

              3. Question 3 of 7What type does TypeScript give where inside this list listener?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const list = document.createElement("ul");
                
                list.addEventListener("click", (event) => {
                  const where = event.currentTarget;
                });

                Choose an answer to see the explanation.

              4. Question 4 of 7Which line does TypeScript mark in this button code?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function showKey(event: KeyboardEvent) {
                  console.log(event.key);
                }
                
                const addButton = document.createElement("button");
                addButton.addEventListener("click", showKey);

                Choose an answer to see the explanation.

              5. Question 5 of 7What does TypeScript say about this misspelled event name?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const addButton = document.createElement("button");
                
                addButton.addEventListener("clik", (event) => {
                  console.log(event.clientX);
                });

                Choose an answer to see the explanation.

              6. Question 6 of 7What does this custom event print, and does TypeScript complain?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const added = new CustomEvent<{ count: number }>("cart:update");
                console.log(added.detail);

                Choose an answer to see the explanation.

              7. Question 7 of 7A handler typed (event: Event) => void is attached with addEventListener("click", handler). What does TypeScript say?

                Choose an answer to see the explanation.

              Key takeaways

              • The name you pass to addEventListener picks the event's type: "keydown" gives a KeyboardEvent, "click" a PointerEvent.
              • TypeScript looks the name up in lists like HTMLElementEventMap. A name it doesn't know gives a plain Event, with no error on the name.
              • event.target and event.currentTarget are both EventTarget | null. Use your element variable, or check with instanceof.
              • 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 the detail you 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.

              CompleteFrontend Clear concepts. Working examples.