Recipe · Node and platforms

How do I type event handlers and addEventListener in JavaScript?

  • Browser (no bundler)
  • DOM
  • addEventListener
  • Events
  • Checked with TypeScript 5.9

The problem In my input handler, event.target.value fails because target is an EventTarget, and new FormData(event.currentTarget) is refused. How should event handlers be typed in a checked .js file?

The short answer

Narrow the element first, then use that variable inside the listener. The event’s own type comes from the event name:

search.js search.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
const form = document.querySelector("form");
const input = document.querySelector("input[type=search]");

if (form && input instanceof HTMLInputElement) {
  input.addEventListener("input", () => {
    form.dataset.empty = String(input.value.trim() === "");
  });

  form.addEventListener("submit", (event) => {
    event.preventDefault();
    const data = new FormData(form);
    console.log(data.get("q"));
  });

  document.addEventListener("keydown", (event) => {
    if (event.key === "/" && !event.ctrlKey) input.focus();
  });
}

/** @param {PointerEvent} event */
function onCartClick(event) {
  if (event.target instanceof HTMLButtonElement) event.target.disabled = true;
}
document.getElementById("cart")?.addEventListener("click", onCartClick);
const form = document.querySelector("form");
const input = document.querySelector("input[type=search]");

if (form && input instanceof HTMLInputElement) {
  input.addEventListener("input", () => {
    form.dataset.empty = String(input.value.trim() === "");
  });

  form.addEventListener("submit", (event) => {
    event.preventDefault();
    const data = new FormData(form);
    console.log(data.get("q"));
  });

  document.addEventListener("keydown", (event) => {
    if (event.key === "/" && !event.ctrlKey) input.focus();
  });
}

function onCartClick(event: PointerEvent): void {
  if (event.target instanceof HTMLButtonElement) event.target.disabled = true;
}
document.getElementById("cart")?.addEventListener("click", onCartClick);
Listeners typed from the event name, and the element taken from the closure.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "lib": ["es2022", "dom", "dom.iterable"],
    "types": []
  },
  "include": ["*.js"]
}

"submit" gives a SubmitEvent, "keydown" a KeyboardEvent (so event.key exists), and a named handler can declare its event type with @param. When the element comes from event.target, an instanceof check narrows it.

Why it works

TypeScript’s DOM library has an event map per object (HTMLElementEventMap, DocumentEventMap, WindowEventMap). addEventListener("keydown", …) looks the name up in the map and types the listener’s event parameter, so inline arrow functions need no JSDoc. The element the listener belongs to is already narrowed in the enclosing code, and the closure keeps that type.

The same thing in TypeScript

The TypeScript equivalent tab differs only in onCartClick(event: PointerEvent). The listeners themselves are inferred the same way in both languages.

What goes wrong

event.target is EventTarget | null, so reading .value from it fails twice (TS18047, TS2339); event.currentTarget has the same type, which FormData refuses (TS2345); and a resize event is a UIEvent, which has no key:

search.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: search.js(4,15): error TS18047: 'event.target' is possibly 'null'. search.js(4,28): error TS2339: Property 'value' does not exist on type 'EventTarget'. search.js(10,29): error TS2345: Argument of type 'EventTarget | null' is not assignable to parameter of type 'HTMLFormElement | undefined'. Type 'null' is not assignable to type 'HTMLFormElement | undefined'. search.js(16,21): error TS2339: Property 'key' does not exist on type 'UIEvent'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. search.js(4,15): error TS18047: 'event.target' is possibly 'null'. search.js(4,28): error TS2339: Property 'value' does not exist on type 'EventTarget'. search.js(10,29): error TS2345: Argument of type 'EventTarget | null' is not assignable to parameter of type 'HTMLFormElement | undefined'. Type 'null' is not assignable to type 'HTMLFormElement | undefined'. search.js(16,21): error TS2339: Property 'key' does not exist on type 'UIEvent'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. search.js(4,15): error TS18047: 'event.target' is possibly 'null'. search.js(4,28): error TS2339: Property 'value' does not exist on type 'EventTarget'. search.js(10,29): error TS2345: Argument of type 'EventTarget | null' is not assignable to parameter of type 'HTMLFormElement | undefined'. Type 'null' is not assignable to type 'HTMLFormElement | undefined'. search.js(16,21): error TS2339: Property 'key' does not exist on type 'UIEvent'. checked 21 October 2026
const input = document.querySelector("input");
input?.addEventListener("input", (event) => {
  console.log(event.target.value);error TS18047: 'event.target' is possibly 'null'.error TS2339: Property 'value' does not exist on type 'EventTarget'.
});

document.querySelector("form")?.addEventListener("submit", (event) => {
  event.preventDefault();
  const data = new FormData(event.currentTarget);error TS2345: Argument of type 'EventTarget | null' is not assignable to parameter of type 'HTMLFormElement | undefined'.
  Type 'null' is not assignable to type 'HTMLFormElement | undefined'.
  console.log(data);
});

window.addEventListener("resize", (event) => {
  console.log(event.key);error TS2339: Property 'key' does not exist on type 'UIEvent'.
});
event.target and event.currentTarget are not the element.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "lib": ["es2022", "dom", "dom.iterable"],
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • click is a PointerEvent in TypeScript 5.9’s DOM types. A handler declared with @param {MouseEvent} event is still accepted, since a PointerEvent is a MouseEvent.
  • Listeners on a plain Element lose the map. document.querySelector(".cart") returns Element, whose map lacks "click", so a handler with a typed event parameter is refused; select by id or tag, or narrow with instanceof first.
  • this in a function listener is the element. A function (not an arrow) listener gets this typed as the element; see typing this.

Questions

Why is event.target not the element I added the listener to?

Events bubble: target is where the event started, which can be any descendant (or a text node's parent). currentTarget is the element with the listener, but TypeScript's DOM types still declare it as EventTarget | null, because the same event object travels through many elements.

How do I type a custom event's detail?

Create it as new CustomEvent("cart:add", { detail: { sku } }) and give listeners the type with @param {CustomEvent<{ sku: string }>} event. For addEventListener to infer it from the name, the event map must be extended in a .d.ts file, like the tag name map in the custom elements recipe.

Sources

Last reviewed 21 October 2026.