I am having some issues trying to overload the addEventListener here, when trying to add/remove eventListener in this useEffect, the following error is encountered:
No overload matches this call. Overload 1 of 2, '(type: 1"focusin\", listener: (this: Document, ev: FocusEvent) => any, options?: boolean | AddEventListenerOptions | undefined): void', gave the following error. Argument of type '(e: FocusEvent) => void' is not assignable to parameter of type '(this: Document, ev: FocusEvent) => any'. Types of parameters 'e' and 'ev' are incompatible. Type 'FocusEvent' is missing the following properties from type 'FocusEvent<Element, Element>': nativeEvent, isDefaultPrevented, isPropagationStopped, persist Overload 2 of 2, '(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined): void', gave the following error. Argument of type '(e: FocusEvent) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject'.
import { useState, FocusEvent, useEffect } from 'react';
interface Element {
removeEventListener(type: 'focus' | 'focusin' | 'focusout', listener: (event: FocusEvent) => any, options?: boolean | EventListenerOptions): void;
addEventListener(type: 'focus' | 'focusin' | 'focusout', listener: (event: FocusEvent) => any, options?: boolean | EventListenerOptions): void;
}
const useActiveElement = () => {
const [active, setActive] = useState<EventTarget>();
const handleFocusIn = (e: FocusEvent) => {
if (!e.target) return;
if (e.target.tagName === 'BUTTON') return;
setActive(e.target);
};
useEffect(() => {
document.addEventListener('focusin', handleFocusIn); //This line
return () => {
document.removeEventListener('focusin', handleFocusIn); //This line
};
}, []);
return active;
};
We are running typescript on nextjs 13's intial settings, which is strict
We tried to overload with the interface above, but it did not help. We've tried looking at docs but can't find anything, and searching the error gives a few results, none of which has helped. Any help would be much appreciated
CodePudding user response:
Instead of overloading, you probably would rather want to infer the property from the document type.
The problem with your code is that you are trying to assume the signature of the addEventListener callback, although you didn't manage to, yet trying to override it would require to override the Document api, which is something that should be avoided here; you usually override the Document api if you need to add something that comes from external libraries of it, like libraries adding something to the window
or document
object.
The correct way to procede in these scenarios, in my opinion, is the following:
- Inspect the signature and try to guess where it is coming from: in this case, you can see that the type of
document.addEventListener
isDocument.addEventListener<T>
, where the genericT
is a string that represents all the possible events ofaddEventListener
. Because of that, the type of our event listener is going to beDocument.addEventListener<'focusin'>
. - From the above function, we want to get the type of the second argument of it. For that we could use
infer
, but luckily out of box typescript actually has an utility type namedParameters
, which conveniently returns the type of the arguments of a generic type. So, to get the parameters, we doParameters<Document['addEventListener']>
. - From the parameters, we want to get the type of the second argument, hence, since the arguments are Iterable, you can just get the index 1 of it (second element), hence our expression becomes:
Parameters<Document['addEventListener']>[1]
, which literally means "Take the type of the second argument of the function Document.addEventListener and return it". This will end up with a signature which is compatible with the second argument ofdocument.addEventListener
.
As a side note, the only problem we still will have is that e.target
is going to be an EventTarget
, while you want to act on it in specific cases, like if it actually is an HTMLButtonElement.
For that kind of situations, I personally recommend using type guards, which lets you have further control on it. For your case specifically, you can do this:
function isEventTargetButton(target: EventTarget): target is HTMLButtonElement {
return (target as HTMLButtonElement).tagName === 'BUTTON';
}
So that when you use it in your code, e.target will be inferred properly:
if (e.target && isEventTargetButton(e.target) {
// e.target is `HTMLButtonElement`, type completion will work as intended.
}
// Here, e.target will be `EventTarget`.
So, to summarize, your code will look like something like this:
function isEventTargetButton(target: EventTarget): target is HTMLButtonElement {
return (target as HTMLButtonElement).tagName === 'BUTTON';
}
const useActiveElement = () => {
const [active, setActive] = useState<EventTarget>();
const handleFocusIn: Parameters<Document['addEventListener']>[1] = (e) => {
if (!e.target) return;
if (isEventTargetButton(e.target)) return;
setActive(e.target);
};
useEffect(() => {
document.addEventListener('focusin', handleFocusIn); //This line
return () => {
document.removeEventListener('focusin', handleFocusIn); //This line
};
}, []);
return active;
};
A final side note is that you should make a specific type for Parameters<Document['addEventListener']>[1]
for readability purposes. I'm letting the above example as-is, although making it more readable is definitely something you want to do.
A stackblitz to play around with it is here: https://stackblitz.com/edit/next-typescript-pv65sz?file=hooks/useActiveElement.ts