Chapter 9.4☕ 14 min read

Event Typing

Each event type gives you the right properties, like choosing the right CCTV feed at the checkpost.

01The Right CCTV Feed

Events are at the heart of interactive web applications. Every click, keypress, form submission, scroll, and touch is an event. And in TypeScript, every event has a SPECIFIC type that determines what properties you can access. Using the wrong event type is like watching the wrong CCTV feed at an RTA checkpost — you miss the crucial details you need.

Imagine the RTA control room. There are multiple CCTV feeds on the wall:

  • The speed camera feed shows vehicle speeds (MouseEvent — gives you clientX, clientY, button).
  • The license plate camera shows plate numbers (KeyboardEvent — gives you key, code, ctrlKey).
  • The traffic light camera shows signal status (InputEvent — gives you data, inputType).
  • The general overview camera shows the road (Event — the base type, limited properties).

If you look at the speed camera feed to check a license plate, you won't see the plate clearly — wrong camera for the job. Similarly, if you type a keydown event as MouseEvent, you won't have access to .key or .code. TypeScript enforces that you use the right event type for the right event, ensuring you have access to the correct properties at compile time. Each event type is a specialized tool designed for a specific interaction — choose wisely and let TypeScript guide you to the right one.

02Event Type Hierarchy

TypeScript's DOM type definitions include a rich hierarchy of event types. Each one extends the base Event class and adds specific properties:

// Base type — minimal properties
Event
  .type, .target, .currentTarget
  .preventDefault(), .stopPropagation()

// UI events — adds view and detail
UIEvent extends Event
  .view, .detail

// Mouse events — pointer interactions
MouseEvent extends UIEvent
  .clientX, .clientY, .button
  .altKey, .ctrlKey, .shiftKey

// Keyboard events — key presses
KeyboardEvent extends UIEvent
  .key, .code, .repeat
  .altKey, .ctrlKey, .shiftKey

// Focus events — element focus
FocusEvent extends UIEvent
  .relatedTarget

// Input events — value changes
InputEvent extends UIEvent
  .data, .inputType, .isComposing

The event type is determined by TWO things: the element the listener is attached to, and the event name string. TypeScript defines overloaded versions of addEventListener for each combination:

// TypeScript's addEventListener overloads:
element.addEventListener(
  "click",
  handler: (e: MouseEvent) => void
);
element.addEventListener(
  "keydown",
  handler: (e: KeyboardEvent) => void
);
element.addEventListener(
  "input",
  handler: (e: InputEvent) => void
);

// The element type ALSO matters:
// button.addEventListener("click")
// → MouseEvent
// But window.addEventListener("click")
// → MouseEvent (same, since click is
//   always a mouse event)

TypeScript knows that when you attach a "click" event to a button, the handler receives a MouseEvent. When you attach a "keydown" event to an input, it's KeyboardEvent. This mapping is built into TypeScript's type definitions and works automatically. You rarely need to annotate the event parameter — TypeScript infers it from the element and event name.

03Typing Event Handlers

There are several ways to handle events in TypeScript. Each approach has different ergonomics for typing the event parameter.

Method 1: addEventListener (most flexible)

const button =
  document.querySelector("button");
if (button) {
  button.addEventListener(
    "click", (e) => {
      // e is inferred as MouseEvent!
      console.log(
        e.clientX, e.clientY
      );
    }
  );
}

const input =
  document.querySelector("input");
if (input) {
  input.addEventListener(
    "keydown", (e) => {
      // e is inferred as KeyboardEvent!
      if (e.key === "Enter") {
        console.log("Pressed Enter!");
      }
    }
  );
}

Method 2: Inline Handlers (onclick, oninput, etc.)

// Inline handlers also type the event
button.onclick = (e) => {
  // e is MouseEvent
  console.log(e.clientX);
};

input.oninput = (e) => {
  // e is InputEvent
  console.log(e.data);
};

input.onkeydown = (e) => {
  // e is KeyboardEvent
  if (e.key === "Escape") {
    input.blur();
  }
};

Method 3: In Angular/React Templates

// Angular
// (click)="handleClick($event)"
// $event is MouseEvent

// React
// <button onClick={(e: React.MouseEvent) => {}}>
// React uses SyntheticEvent wrappers

Method 4: Custom Events

For custom events dispatched with new CustomEvent(), you can use the CustomEvent<T> generic type where T is the detail property type:

// Dispatch a custom event
const event = new CustomEvent<UserData>(
  "user-login",
  { detail: { id: 1, name: "Imran" } }
);
window.dispatchEvent(event);

// Listen with typed detail
window.addEventListener(
  "user-login",
  (e: CustomEvent<UserData>) => {
    // e.detail is UserData
    console.log(e.detail.name);
  }
);

TypeScript's event inference is comprehensive and covers virtually all standard DOM events. Let the inference work for you — annotate only when the context is ambiguous or when using custom events.

04Event Typing Traps

Event typing has a few common gotchas that can lead to frustrating compile errors or incorrect type assumptions.

Trap 1: Using the Base Event Type

When you annotate an event as Event instead of letting TypeScript infer the specific type, you lose access to type-specific properties:

button.addEventListener(
  "click", (e: Event) => {
    // e.clientX — ❌ not on Event
    // e.button — ❌ not on Event
  }
);

// FIX: Let TS infer or use specific type:
button.addEventListener(
  "click", (e: MouseEvent) => {
    console.log(e.clientX); // ✅
  }
);

Trap 2: this in Event Handlers

When you use a class method as an event handler, this might not refer to the class instance. Use arrow functions or .bind(this) to preserve context:

class FormHandler {
  button = document.querySelector("button")!;

  handleClick(e: MouseEvent) {
    // this might be the button, NOT the class!
    console.log(this); // HTMLButtonElement!
  }

  constructor() {
    // ❌ this is wrong in handleClick
    this.button.addEventListener(
      "click", this.handleClick
    );

    // ✅ Use arrow function
    this.button.addEventListener(
      "click", (e) => this.handleClick(e)
    );
  }
}

Trap 3: React Event Types are Different from DOM Types

React wraps native DOM events in SyntheticEvent objects. The property names are the same but the types are different. Always use React.MouseEvent, React.KeyboardEvent, etc. in React code:

// React — NOT DOM!
function handleClick(
  e: React.MouseEvent<HTMLButtonElement>
) {
  console.log(e.clientX);
}

// The generic parameter is
// the element type!
// <HTMLButtonElement>
// <HTMLInputElement>
// <HTMLDivElement>

Trap 4: target vs currentTarget

e.target is typed as EventTarget | null — it's the element that ORIGINATED the event. e.currentTarget is typed as the specific element type — it's the element the listener is attached to:

button.addEventListener(
  "click", (e) => {
    // e.currentTarget is HTMLButtonElement
    // e.target is EventTarget | null

    // e.target might be a child element!
    // Always narrow e.target:
    if (e.target instanceof HTMLElement) {
      console.log(e.target.tagName);
    }
  }
);
05Event Typing Cheatsheet

Here's your complete cheatsheet for event typing in TypeScript. Pin this to your mental board!

Common Event Types by Name:

click, dblclick, mousedown
  → MouseEvent
keydown, keyup, keypress
  → KeyboardEvent
input, change
  → InputEvent / Event
submit → SubmitEvent
focus, blur → FocusEvent
scroll, resize → Event
touchstart, touchend → TouchEvent
dragstart, drag, dragend → DragEvent

Adding Event Listeners:

// addEventListener (TS infers type)
element.addEventListener("click", (e) => {
  // e is MouseEvent — inferred!
});

// Inline handler
element.onclick = (e: MouseEvent) => {
  // e is MouseEvent
};

// Custom event
element.addEventListener(
  "custom",
  (e: CustomEvent<T>) => {
    // e.detail is T
  }
);

Key Rules:

  • Let TypeScript infer the event type from addEventListener — don't over-annotate
  • Use the most specific event type for full property access (MouseEvent vs Event)
  • Use KeyboardEvent.key for character values, .code for physical keys
  • React uses SyntheticEvent subtypes — import from React types
  • Custom events use CustomEvent<T> where T is the detail type
  • Use e.currentTarget for the element with the listener, narrow e.target

The Golden Rule: "Event typing is like choosing the right CCTV feed at an RTA checkpost. MouseEvent is the speed camera, KeyboardEvent is the license plate camera, InputEvent is the traffic light camera. Pick the right feed, and you'll see exactly what you need, bhai!"

Key Takeaways

  • Each DOM event has a specific type — MouseEvent, KeyboardEvent, InputEvent, etc.
  • TypeScript infers the event type from the element and event name in addEventListener
  • Use the specific event type (not the base Event) to access all relevant properties
  • React uses SyntheticEvent subtypes — import from React types, not DOM types
  • Custom events use CustomEvent where T is the detail property type
  • e.currentTarget is the element with the listener; narrow e.target before using it
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase