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