TypeScript, event handlers in the DOM, and the this keyword

In this quick post you'll learn how to make TypeScript play well with the infamous this keyword when working with event handlers in the DOM.

TypeScript, event handlers in the DOM, and the this keyword

What is this in JavaScript?

this in JavaScript is a magic keyword for: "whichever object a given function runs in". Consider the following object and its nested function:

const person = {
  name: "Jule",
  printName: function() {
    console.log(this.name);
  }
};

When I call person.printName(), this will point to the person object. this is everywhere in JavaScript, including event handler functions in the DOM.

What are event handlers in JavaScript?

The Document Object Model is a convenient representation of every element in an HTML page. Browsers keep this structure in memory and expose a lot of methods for interacting with the DOM.

HTML elements in the DOM are not static. They are connected to a primordial object named EventTarget which lends them three methods:

  • addEventListener
  • removeEventListener
  • dispatchEvent

Whenever an HTML element is clicked, the most simple case, an event is dispatched. Developers can intercept these events (JavaScript engines are event-driven) with an event listener.

Event listeners in the DOM have access to this because the function runs in the context object who fired up the event (an HTML element most of the times). Consider the following snippet:

const button = document.querySelector("button");
button.addEventListener("click", handleClick);

function handleClick() {
    console.log("Clicked!");
    this.removeEventListener("click", handleClick);
}

Here removeEventListener is called on the HTML button who triggered the click event. Now let's see what happens when we convert this code to TypeScript.

TypeScript and this

When converting our code to TypeScript the IDE and the compiler will complain with two errors:

error TS2531: Object is possibly 'null'.
error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.

We can get rid of the first error with optional chaining, landed in TypeScript > 3.7:

const button = document.querySelector("button");
// optional chaining
button?.addEventListener("click", handleClick);

function handleClick() {
    console.log("Clicked!");
    this.removeEventListener("click", handleClick);
}

For the second error to go away instead, this must appear as the first parameter in the handler signature, with the appropriate type annotation. HTMLElement is enough in this case:

const button = document.querySelector("button");
button?.addEventListener("click", handleClick);

function handleClick(this: HTMLElement) {
    console.log("Clicked!");
    this.removeEventListener("click", handleClick);
}

You might have guessed how the "trick" is applicable to any function dealing with this, not necessarily an event handler (don't mind any here):

function aGenericFunction(this: any, key: string) {
  return this.doStuff(key);
}

const aFictionalObject = {
  first: "a",
  second: "b",
  doStuff: function(str: string) {
    return `${this.first} ${str}`;
  }
};

aGenericFunction.call(aFictionalObject, "appendMe");

Thanks for reading and stay tuned!

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!