The short answer
Add @this {Type} to the function’s JSDoc. Here the handler is attached to buttons, so this is an HTMLButtonElement:
/**
* @this {HTMLButtonElement}
* @param {MouseEvent} event
*/
function toggleSaved(event) {
const saved = this.classList.toggle("saved");
this.textContent = saved ? "Saved" : "Save";
if (event.shiftKey) this.blur();
}
for (const button of document.querySelectorAll("button")) {
button.addEventListener("click", toggleSaved);
}function toggleSaved(this: HTMLButtonElement, event: MouseEvent): void {
const saved = this.classList.toggle("saved");
this.textContent = saved ? "Saved" : "Save";
if (event.shiftKey) this.blur();
}
for (const button of document.querySelectorAll("button")) {
button.addEventListener("click", toggleSaved);
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"lib": ["es2022", "dom", "dom.iterable"],
"types": []
},
"include": ["*.js"]
} Inside toggleSaved, this.classList and this.blur() are checked against the button type, and addEventListener accepts the function because its this matches the element the listener is added to.
Why it works
JavaScript decides this when a function is called, not when it is written, so the checker cannot work it out from the function alone. @this {HTMLButtonElement} is the JSDoc form of TypeScript’s fake first parameter, this: HTMLButtonElement: it types this in the body, and it is checked at every call, which must supply a matching this.
The same thing in TypeScript
The TypeScript equivalent tab declares function toggleSaved(this: HTMLButtonElement, event: MouseEvent). The this parameter is not a real argument; it disappears when the code runs, which is why JavaScript needs a JSDoc tag for it instead.
What goes wrong
Without the tag, strict (through noImplicitThis) reports the first use of this (TS2683):
/** @param {MouseEvent} event */
function toggleSaved(event) {
const saved = this.classList.toggle("saved");error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.
return saved && event.shiftKey;
}
for (const button of document.querySelectorAll("button")) {
button.addEventListener("click", toggleSaved);
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"lib": ["es2022", "dom", "dom.iterable"],
"types": []
},
"include": ["*.js"]
} With the tag, the calls are checked instead. A plain call supplies no this at all, so it fails with TS2684:
/**
* @this {HTMLButtonElement}
* @param {MouseEvent} event
*/
function toggleSaved(event) {
this.classList.toggle("saved");
return event.shiftKey;
}
toggleSaved(new MouseEvent("click"));error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'HTMLButtonElement'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"lib": ["es2022", "dom", "dom.iterable"],
"types": []
},
"include": ["*.js"]
} Call it with toggleSaved.call(button, event), or let addEventListener call it, and the error goes away.
Gotchas
@thisbelongs to normal functions. On an arrow function it is an error (TS2730, “An arrow function cannot have a ‘this’ parameter”), because arrow functions never get athisof their own.- The element type must match.
button.addEventListener("click", toggleSaved)passes because the button is anHTMLButtonElement. Adding the same listener to a<div>, or to the plainElementthatquerySelector(".save")returns, fails with TS2769; narrow the element withinstanceof HTMLButtonElementfirst. That check leans on the typedeventparameter: a handler with no parameters also matchesaddEventListener’s catch-all signature, and the wrong element goes unreported (the@thispage shows that case). - A constructor function is different. A
functionthat assigns tothis.xis treated as a constructor, so itsthisis never an implicitany; a class is the clearer way to write it.
Questions
Do methods in a class or an object literal need @this?
No. In a class method this is the instance, and in a method of an object literal it is that object; tsc knows both. @this is for standalone functions whose this is set by the caller, such as event handlers and functions borrowed with call.
Should I use an arrow function instead?
An arrow function has no this of its own; it uses the surrounding one. For a handler that needs the element, a normal function with @this works, or an arrow function that takes the element from event.currentTarget.
Sources
- JSDoc Reference (@this) · TypeScript handbook
- More on Functions (declaring this in a function) · TypeScript handbook
- this · MDN Web Docs
Last reviewed 21 October 2026.