-
Notifications
You must be signed in to change notification settings - Fork 12.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TypeScript doesn't allow event : CustomEvent in addEventListener #28357
Comments
I can't repo this with the TypeScript 3.1.4: const button = document.createElement('button')
button.addEventListener('myCustomEvent', (event: CustomEvent) => {
//do something
});
|
I'm writing my code using https://stenciljs.com/ which is reporting the following TypeScript version:
My tsconfig.json file looks like this:
|
In the lib.dom.d.ts file I have the following definition for HTMLElement's addEventListener:
where:
and
How is the addEventListener defined in your version of TypeScript? and how would I update it on my PC if it is a dependency of Stencil. In my package.json, I just have the following devDependencies defined, note TypeScript is not listed anywhere:
|
I have always needed to write it like this to avoid the issue with custom events: buttonEl.addEventListener('myCustomEvent', ((event: CustomEvent) => {
//do something
}) as EventListener); |
|
What is the proper solution to this - or what is the reason why it creates an error in the first place? |
I have been using msheakoski's solution. It is verbose but works. Ideally the EventListenerOrEventListenerObject would be updated to include CustomEventListener |
I can confirm that I still have to do this. Right now, I have something like this: variableFromTheScopeOfTheFunction = 'some parameter';
...
...
['click', 'touchend'].forEach(handler => document.addEventListener(handler, this.genEventTrigger(this.variableFromTheScopeOfTheFunction))); genEventTrigger(param: any) {
return (event: Event) => {
// const someVar = this.variableFromTheScopeOfTheFunction; <- can't do this because, 'this' here will refer to the document, not the scope of the function itself
const someVar = param; // have to do this INSTEAD, so it's set during compile time
// do things here
};
} At this point, Typescript complains that the function signature is invalid for an EventListener. Adding ['click', 'touchend'].forEach(handler => document.addEventListener(handler, this.genEventTrigger('a compile time parameter') as EventListener)); It's quite silly. Unless I could be doing something better, feel free to correct me! |
I use an other workaround which keep a type guard in the addEvenListener. Because the interface of EventListener is contravariance rules we need to check if the event receive in our addEvenlistener contains the params detail. function isCustomEvent(evt: Event): evt is CustomEvent { |
I also have this issue - I want to define a subclass of Event that has custom fields on it. If there was a generic type argument for EventListener maybe that would help? currently I have to do: interface IUseWebSocketClientArgs {
onEvent?: (evt: WSEvent) => void
}
...
client.addEventListener(WEBSOCKET_EVENT, onEvent as EventListener) something like this might work? interface IUseWebSocketClientArgs {
onEvent?: EventListener<WSEvent>
}
...
client.addEventListener(WEBSOCKET_EVENT, onEvent) |
addEventListener<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions
): void; so, for instance if you want a key/value map of events in an object: type Events = {
[K in keyof HTMLElementEventMap]:
(this: HTMLElement, event: HTMLElementEventMap[K]) => void
}; |
@ weswigham Assigning to you for triage. Sorry, I forgot to remove my assignment when transferring this issue to the TS repo so it never had proper followup |
I'm experiencing this issue too. i can dispatch custom event without type errors window.dispatchEvent(new CustomEvent("name", { detail: "detail" })); but can't receive // TS2339: Property 'detail' does not exist on type 'Event'.
window.addEventListener("name", ({ detail }) => {
// do magic
}); there are two ways to fix this 1 parametrize window.addEventListener<CustomEvent>("name", ({ detail }) => {
// do magic
}); 2 or change signature in addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
...
declare type EventListenerOrEventListenerObject = EventListener | EventListenerObject;
...
interface EventListenerObject {
handleEvent(evt: Event | CustomEvent): void; // !!!
} |
I'm having the same issue, I was trying to do Module Augmentation on "lib.dom.d.ts" but I couldn't find a way so far :( It would be really handy if we could have full support for custom events in typescript. |
this is working for me:
The only other thing I could get to work is @msheakoski 's
I can't decide which I don't like the least :( |
Different means to the same end:
|
I am quite late to the party, but for anyone googling to this question, this is what I find out (global augmentation): declare global {
// note, if you augment `WindowEventMap`, the event would be recognized if you
// are doing window.addEventListener(...), but element would not recognize I believe;
// there are also
// - ElementEventMap, which I believe you can document.addEventListener(...)
// - HTMLElementEventMap (extends ElementEventMap), allows you to element.addEventListener();
interface WindowEventMap {
"custom-event": CustomEvent<{ data: string }>;
}
}
window.addEventListener("custom-event", event => {
const { data } = event.detail; // ts would recognize event as type `CustomEvent<{ data: string }>`
}) |
This works if you type the properties in the custom event as potentially optional for compatibility with interface SomeCustomEvent extends Event {
detail?: {
nestedProperty: boolean;
}
}
window.addEventListener('someCustomEvent', (event: SomeCustomEvent) => {
if (event.detail) {
// no type errors for SomeCustomEvent
// event.detail is defined inside conditional type guard
}
}) This option seems preferable to the verbose workarounds above, but perhaps I am missing something about using |
So I would like summarize here what I have learned so far and try to propose a way to actually fix this, hopefully following the way Typescript has implemented this in the first place and without breaking anything. From what I gather on this thread and other sources, the Typescript recommended way to add a custom event would be to modify the event map of a child of EventTarget. The Typescript approach is a bit lengthily but seems simple enough and works. So if we wanted to add a custom event for Window we just globally declare a custom event on the WindowEventMap and we are good to go, here is an example by @jorge-ui. The same approach could be used for most, if not all, children of EventTarget, e.g. Simple, consistent and works. The problem is that the father of all those interfaces, EventTarget itself, does not have it's own event map. So custom events can't be created in the same way as all it's children, yet it can be used and it's indeed used as a general purpose event emitter. To fix this, two things should be done
Thoughts? |
Replaces the node `EventEmitter` with the pure-js `EventTarget` class. All events are now instances of [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event). For typing [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) can be used which gives us a typed `.detail` field. Tediously `EventTarget` itself [doesn't support typed events](microsoft/TypeScript#28357) (yet?) and node.js [doesn't support](nodejs/node#40678) `CustomEvent` globally so bit of trickery was required but hopefully this can be removed in future: ```js import { EventEmitter, CustomEvent } from '@libp2p/interfaces' interface EventMap { 'foo': CustomEvent<number> } class MyEmitter extends EventEmitter<EventMap> { // ... other code here } const emitter = new MyEmitter() emitter.addEventListener('foo', (evt) => { const n = evt.detail // n is a 'number' }) ```
You can write the event types as static properties of your custom event classes, making it less verbose (also no need to guess all those variable names anymore, you can always use // "CustomEvent" comes from 'lib.dom.d.ts' (tsconfig.json)
export class MyCustomEvent extends CustomEvent<User> {
static type: "my-custom-event" = "my-custom-event";
constructor(detail: User) {
super(MyCustomEvent.type, { detail });
}
} |
Can anyone point to an example that's no
The code above fails when adding the listener
I don't quite get how to extend my own class for this custom event map |
Okay, I think I worked it out
|
This issue is still relevant, there are only workarounds in this thread to achieve this:
|
I found this thread while intending to file a new bug report. Would an admin maybe tag this thread as a bug? Here is the problem illustrated on the typescript playground. Typescript v4.7.4 It is persisting into 4.8Beta and Nightly. |
I regard this as a bug. MDN states that you can dispatch a This works on all modern browsers. The interface is typed as
interface EventListener {
(evt: Event): void;
}
interface EventListenerObject {
handleEvent(object: Event): void;
} That is a mismatch. It should be - (evt: Event): void;
+ (evt: Event | CustomEvent): void; and - handleEvent(object: Event): void;
+ handleEvent(object: Event | CustomEvent): void; What's curious though is that the WHATWG specs do not reflect this: https://dom.spec.whatwg.org/#interface-eventtarget Maybe this is the reason / root cause? |
I'm seeing this issue as well. This bug seems to have existed for quite a while now. |
I also want to report that this is definitely a bug in my opinion. Here I want to supply a listener that is designed to receive
It would be nice to not need the type assertion here. |
This thread has several suggestions to extend EventMap interfaces to define your own custom event. I think that's the way to go, because otherwise things may break whenever HTML define a new event, which would then suddenly fire non-custom events. Edit: See also microsoft/TypeScript-DOM-lib-generator#1535 (comment). |
I no longer thing this a bug. See here |
...and if people get to this in the issue, please consider that it is a much better TypeScript pattern to define your types in the object itself, rather than in the callback, which avoids some of the issues that many are having here. |
Here is an example based on the above code. Still noodling on how to best approach the dispachEvents inferrance interface EvMap {
"user:updated": CustomEvent<{ name: string, age: number }>,
}
interface UserInterface extends EventTarget {
addEventListener<K extends keyof EvMap>(event: K, listener: ((this: UserInterface, ev: EvMap[K]) => any) | null, options?: AddEventListenerOptions | boolean): void;
addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean): void;
}
class UserInterface {
updateUser(user: User) {
this.dispatchEvent(new CustomEvent('user:updated', { detail: {
age: 100,
name: 'Wes Bos'
} }));
}
}
const instance = new UserInterface();
instance.addEventListener('user:updated', (event) => {
event.detail.name; // string
event.detail.age; // number
event.detail.doesntExist; // error
}); |
Adding this here for the sake of brevity. Similar to how you'd extend the interface Example {
foo: string;
bar: string;
}
declare global {
interface WindowEventMap {
'custom:event-1': CustomEvent<Example>;
'custom:event-2': CustomEvent<Example>;
}
} |
Confirm that Custom Events working in typescript without custom dom.d.ts file, just use it like this: My types: export const CustomConfirmationEventType = "confirmationDialogConfirm"
export enum YesNoCancel {
YES = 'YES',
NO = 'NO',
CANCEL = 'CANCEL'
}
export interface ConfirmationDialogState {
isOpenConfirm: boolean;
confirmLabel: string;
innerText: string;
confirmYesLabel: string;
confirmNoLabel: string;
}
const initialState: ConfirmationDialogState = {
isOpenConfirm: false,
confirmLabel: "",
innerText: "",
confirmYesLabel: "",
confirmNoLabel: "",
};
export interface CustomConfirmationEvent extends Partial<Event> {
detail?: {
message: string;
}
} My custom hook useConfirmationDialog.ts: import { useAppDispatch } from '@/app/hooks';
import { useCallback } from 'react';
import { ConfirmationDialogState,closeConfirmationDialog,openConfirmationDialog, CustomConfirmationEvent,CustomConfirmationEventType, YesNoCancel}
from '@/store/confirm'
const useConfirmationDialog = () => {
const dispatch = useAppDispatch()
const showConfirmationDialog = useCallback( (props: ConfirmationDialogState) => {
return new Promise<YesNoCancel>((resolve) => {
const handleConfirm:EventListener = (event: CustomConfirmationEvent) => {
//this.[...] // this is Document
//event.detail ... //is your CustomParams type.
//console.log(JSON.stringify(event.detail?.message),event.type) //"YES" confirmationDialogConfirm
dispatch(closeConfirmationDialog())
resolve(event.detail?.message as YesNoCancel)
};
dispatch( openConfirmationDialog(props))
// Підписуємося на події підтвердження та скасування
document.addEventListener(CustomConfirmationEventType, handleConfirm, { once: true })
// Функція очищення
return () => {
document.removeEventListener(CustomConfirmationEventType, handleConfirm)
};
});
},
[dispatch]
);
return { showConfirmationDialog };
};
export default useConfirmationDialog; code in ConfirmationDialog.tsx: const param:ConfirmationDialogState = useAppSelector( selectConfirmationDialog)
const handleConfirm = () => {
const x = new CustomEvent(CustomConfirmationEventType, {detail:{message:YesNoCancel.YES}})
document.dispatchEvent(x)
}
const handleNo = () => {
const x = new CustomEvent(CustomConfirmationEventType, {detail:{message:YesNoCancel.NO}})
document.dispatchEvent(x)
}
const handleCancel = () => {
const x = new CustomEvent(CustomConfirmationEventType, {detail:{message:YesNoCancel.CANCEL}})
document.dispatchEvent(x)
}; and hook useConfirmationDialog used like this: const {showConfirmationDialog} = useConfirmationDialog()
const handleLogout = () => {
showConfirmationDialog(
{
isOpenConfirm: true,
confirmLabel: 'Вихід с кабінету',
innerText: 'Бажаєте вийти з кабінету?\n(Буде очищено кеш та сховище браузера від даних сайту)',
confirmYesLabel: 'Так, вийти',
confirmNoLabel: 'Ні, залишитись',
}
).then((value)=>{
switch (value) {
case YesNoCancel.YES: { dispatch(runLogout(navigate)); return;}
case YesNoCancel.NO: { dispatch(setMessage({message:'З поверненням 😘',open:true})); return;}
case YesNoCancel.CANCEL: {return;}
}
}).catch(()=>{
null
})
}
|
Following up on @wesbos, this is how I solve it for dispatching as well: /**
* Type-safe event listener and dispatch signatures for the custom events
* defined in `TDetails`.
*/
export type CustomEventTarget<TDetails> = {
addEventListener<TType extends keyof TDetails>(
type: TType,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
listener: (ev: CustomEvent<TDetails[TType]>) => any,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener<TType extends keyof TDetails>(
type: TType,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
listener: (ev: CustomEvent<TDetails[TType]>) => any,
options?: boolean | EventListenerOptions,
): void;
dispatchEvent<TType extends keyof TDetails>(
ev: _TypedCustomEvent<TDetails, TType>,
): void;
};
/**
* Extends an element type with type-safe event listener signatures for the
* custom events defined in `TDetails`.
*/
export type CustomEventElement<
TDetails,
TElement = HTMLElement,
> = CustomEventTarget<TDetails> & TElement;
/**
* Internal declaration for the `typeof` trick below.
* Never actually implemented.
*/
declare class _TypedCustomEvent<
TDetails,
TType extends keyof TDetails,
> extends CustomEvent<TDetails[TType]> {
constructor(
type: TType,
eventInitDict: { detail: TDetails[TType] } & EventInit,
);
}
/**
* Typed custom event (technically a typed alias of `CustomEvent`).
* Use with `CustomEventTarget.dispatchEvent` to infer `detail` types
* automatically.
*/
export const TypedCustomEvent = CustomEvent as typeof _TypedCustomEvent; With that setup you can do: // Map of event type -> detail
interface UserEvents {
'user:updated': { name: string; age: number };
}
const div: CustomEventElement<UserEvents> = document.createElement('div');
div.addEventListener('user:updated', (e) => {
// e.detail is inferred to { name: string; age: number };
});
div.dispatchEvent(
new TypedCustomEvent('user:updated', {
// detail is inferred to { name: string; age: number };
detail: { name: 'Bob', age: 42 },
}),
); It has zero runtime overhead ( |
I'm using Visual Studio Code - Insiders v 1.29.0-insider
In my TypeScript project, I'm trying to write the following code:
The problem is that the CustomEvent type gives me the error shown below. If I replace CustomEvent with Event, then there is no error, but then I have difficulty getting event.detail out of the event listener.
}
The text was updated successfully, but these errors were encountered: