Skip to content
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

Open
mmakrzem opened this issue Nov 4, 2018 · 42 comments
Open

TypeScript doesn't allow event : CustomEvent in addEventListener #28357

mmakrzem opened this issue Nov 4, 2018 · 42 comments
Assignees
Labels
Domain: lib.d.ts The issue relates to the different libraries shipped with TypeScript Needs Investigation This issue needs a team member to investigate its status.

Comments

@mmakrzem
Copy link

mmakrzem commented Nov 4, 2018

I'm using Visual Studio Code - Insiders v 1.29.0-insider

In my TypeScript project, I'm trying to write the following code:

buttonEl.addEventListener( 'myCustomEvent', ( event : CustomEvent ) => {
  //do something
} );

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.

"resource": "/c:/Users/me/Documents/app/file.ts",
"owner": "typescript",
"code": "2345",
"severity": 8,
"message": "Argument of type '(event: CustomEvent<any>) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject'.\n  Type '(event: CustomEvent<any>) => void' is not assignable to type 'EventListener'.\n    Types of parameters 'event' and 'evt' are incompatible.\n      Type 'Event' is not assignable to type 'CustomEvent<any>'.\n        Property 'detail' is missing in type 'Event'.",
"source": "ts",
"startLineNumber": 86,
"startColumn": 44,
"endLineNumber": 86,
"endColumn": 72

}

@vscodebot vscodebot bot assigned mjbvz Nov 4, 2018
@mjbvz mjbvz transferred this issue from microsoft/vscode Nov 6, 2018
@mjbvz
Copy link
Contributor

mjbvz commented Nov 6, 2018

I can't repo this with the TypeScript 3.1.4:

const button = document.createElement('button')

button.addEventListener('myCustomEvent', (event: CustomEvent) => {
    //do something
});
  • What TS version are you using?
  • Can you please share your tsconfig.json?

@mmakrzem
Copy link
Author

mmakrzem commented Nov 6, 2018

I'm writing my code using https://stenciljs.com/ which is reporting the following TypeScript version:

C:\Users\me\Documents\work>npm list typescript
my@0.0.1 C:\Users\me\Documents\work
`-- @stencil/core@0.15.2
  `-- typescript@2.9.2

My tsconfig.json file looks like this:

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "allowUnreachableCode": false,
    "declaration": false,
    "experimentalDecorators": true,
    "forceConsistentCasingInFileNames": true,
    "inlineSources": true,
    "jsx": "react",
    "jsxFactory": "h",
    "lib": [
      "dom",
      "es2017",
      "dom.iterable"
    ],
    "moduleResolution": "node",
    "module": "esnext",
    "newLine": "lf",
    "noEmitOnError": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "pretty": true,
    "removeComments": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "es2017"
  },
  "include": [
    "src",
    "types/jsx.d.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

@mmakrzem
Copy link
Author

mmakrzem commented Nov 6, 2018

In the lib.dom.d.ts file I have the following definition for HTMLElement's addEventListener:

addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;

where:

declare type EventListenerOrEventListenerObject = EventListener | EventListenerObject;

and

interface EventListener {
    (evt: Event): void;
}

interface EventListenerObject {
    handleEvent(evt: Event): void;
}

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:

  "devDependencies": {
    "@stencil/core": "^0.15.2",
    "tslint": "^5.11.0"
  },

@weswigham weswigham added Domain: lib.d.ts The issue relates to the different libraries shipped with TypeScript Needs Investigation This issue needs a team member to investigate its status. labels Nov 6, 2018
@msheakoski
Copy link

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);

@saschanaz
Copy link
Contributor

saschanaz commented Nov 8, 2018

strictFunctionTypes causes this issue. Try strictFunctionTypes: false on tsconfig.

@essenmitsosse
Copy link

What is the proper solution to this - or what is the reason why it creates an error in the first place?

@mmakrzem
Copy link
Author

mmakrzem commented Feb 6, 2019

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

@yashsway
Copy link

yashsway commented Apr 10, 2019

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);

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 as EventListener, fixes it:

['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!

@Nikohelie
Copy link

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.
For that, you could define an utils function like that

function isCustomEvent(evt: Event): evt is CustomEvent {
return (evt as CustomEvent).detail !== undefined;
}

@revmischa
Copy link

revmischa commented Aug 1, 2019

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)

@jamie-pate
Copy link

jamie-pate commented Sep 16, 2019

lib.dom.d.ts also uses this definition if it helps you define your signatures:

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
};

@mjbvz mjbvz assigned weswigham and unassigned mjbvz Nov 6, 2019
@mjbvz
Copy link
Contributor

mjbvz commented Nov 6, 2019

@ 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

@TrejGun
Copy link
Contributor

TrejGun commented Jan 23, 2020

I'm experiencing this issue too.
my case is related to chrome plugin where script should communicate with background proccess

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 addEventListener

window.addEventListener<CustomEvent>("name", ({ detail }) => {
  // do magic
});

2 or change signature in lib.dom.d.ts

addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
...
declare type EventListenerOrEventListenerObject = EventListener | EventListenerObject;
...
interface EventListenerObject {
    handleEvent(evt: Event | CustomEvent): void; // !!!
}

@TrejGun
Copy link
Contributor

TrejGun commented Jan 23, 2020

https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail

@jorge-ui
Copy link

jorge-ui commented Mar 6, 2020

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.

@leviwheatcroft
Copy link

leviwheatcroft commented Mar 14, 2020

this is working for me:

interface CustomEvent extends Event {
  detail: string
}
element.addEventListener('myCustomEvent', ({ detail }: CustomEvent) => {
  if (isUndefined(detail)) throw new RangeError()
  doSomething(detail
})

The only other thing I could get to work is @msheakoski 's as EventListener:

buttonEl.addEventListener('myCustomEvent', ((event: CustomEvent) => {
  //do something
}) as EventListener);

I can't decide which I don't like the least :(

@jasonhulbert
Copy link

Different means to the same end:

element.addEventListener('myCustomEvent', (event: Event) => {
    const detail = (event as CustomEvent).detail;
});

@Tundon
Copy link

Tundon commented Jul 27, 2020

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 }>`
})

@hrsh7th
Copy link

hrsh7th commented Dec 17, 2020

I'm writing TypeScript with deno recently and then I met this issue.

I always use @ts-expect-error comment for all places for future improvements of TypeScript.

@chuanqisun
Copy link

chuanqisun commented Dec 20, 2020

You can extend GlobalEventHandlersEventMap

image

If your event originates from an HTMLElement and doesn't bubble, you can extend HTMLElementEventMap so it won't show up on window.
If your event only originates from window, you can extend WindowEventHandlersEventMap so it won't show up on elements.

@michaeljaltamirano
Copy link

michaeljaltamirano commented Mar 3, 2021

This works if you type the properties in the custom event as potentially optional for compatibility with Event, rather than using CustomEvent directly. TypeScript playground link.

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

@carragom
Copy link

carragom commented Apr 12, 2021

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.

  1. HTMLElement => HTMLElementEventMap
  2. AbortSignal => AbortSignalEventMap
  3. Document => DocumentEventMap

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

  1. Create a EventTargetEventMap interface and change EventTarget to use that event map just like all it's children use their own map.
  2. Optionally but I think it makes sense. Change all other event maps to extend EventTargetEventMap so that a custom event created at the EventTargetEventMap level, would propagate to every child up the tree.

Thoughts?

achingbrain added a commit to libp2p/js-libp2p-interfaces that referenced this issue Feb 10, 2022
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'
})
```
@pietrovismara
Copy link

pietrovismara commented Feb 22, 2022

Custom Events with Type Assertion!! 🕺

MyCustomEvent.ts

Playground

// String Literal (type and value) for proper type checking
export const myCustomEventType: "my-custom-event" = "my-custom-event";

// "CustomEvent" comes from 'lib.dom.d.ts' (tsconfig.json)
class MyCustomEvent extends CustomEvent<MyCustomEventDetail> {
    constructor(detail: MyCustomEventDetail) {
        super(myCustomEventType, { detail });
    }
}

type MyCustomEventState = "open" | "update" | "close"

interface MyCustomEventDetail {
    id: number,
    name: string,
    state: MyCustomEventState
}

export default MyCustomEvent;

// augment your global namespace
// here, we're augmenting 'WindowEventMap' from 'lib.dom.d.ts' 👌
declare global {
    interface WindowEventMap {
        [myCustomEventType]: MyCustomEvent
    }
}

Enjoy everyone! :)

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 type):

// "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 });
  }
}

@greggman
Copy link

greggman commented Mar 18, 2022

Can anyone point to an example that's no window? I have my own class that extends EventTarget. How do I set it up so I can use an Event Map?

interface ColorInfo {
  rgb: string;
}

interface CustomEventMap {
  'color': CustomEvent<ColorInfo>;
}

class Foo extends EventTarget {
  doit() {
    this.dispatchEvent(new CustomEvent<ColorInfo>('color', { detail: { rgb: '#112233' } }));
  }
}


const foo = new Foo();
foo.addEventListener('color', (ev: CustomEvent<ColorInfo>) => {
  console.log(ev.detail.rgb);
});

The code above fails when adding the listener

interface ColorInfo
Argument of type '(ev: CustomEvent<ColorInfo>) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject | null'.
  Type '(ev: CustomEvent<ColorInfo>) => void' is not assignable to type 'EventListener'.
    Types of parameters 'ev' and 'evt' are incompatible.
      Type 'Event' is missing the following properties from type 'CustomEvent<ColorInfo>': detail, initCustomEvent(2345)

I don't quite get how to extend my own class for this custom event map

@greggman
Copy link

Okay, I think I worked it out

interface FizzInfo {
  amount: string;
}

interface BuzzInfo {
  level: number;
}

interface FizzBuzzEventMap {
  fizz: CustomEvent<FizzInfo>;
  buzz: CustomEvent<BuzzInfo>;
}

interface FizzerBuzzer extends EventTarget {
  addEventListener<K extends keyof FizzBuzzEventMap>(type: K, listener: (this: FizzerBuzzer, ev: FizzBuzzEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void;
  addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
  removeEventListener<K extends keyof FizzBuzzEventMap>(type: K, listener: (this: FizzerBuzzer, ev: FizzBuzzEventMap[K]) => void, options?: boolean | EventListenerOptions): void;
  removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}

class FizzerBuzzer extends EventTarget {
  numFizz: number = 0;
  numBuzz: number = 0;

  start(): void {
    setInterval(() => this.emitFizz(), 3000);
    setInterval(() => this.emitBuzz(), 5000);
  }
  emitFizz(): void {
    ++this.numFizz;
    this.dispatchEvent(new CustomEvent<FizzInfo>('fizz', {
      detail: { amount: this.numFizz.toString() },
    }));
  }
  emitBuzz(): void {
    ++this.numBuzz;
    this.dispatchEvent(new CustomEvent<BuzzInfo>('buzz', {
      detail: { level: this.numBuzz },
    }));
  }
}


const fb = new FizzerBuzzer();
fb.addEventListener('fizz', (ev) => {
  console.assert(typeof ev.detail.amount === 'string', 'bad!');
  console.log(ev.detail.amount);
});
fb.addEventListener('buzz', (ev) => {
  console.assert(typeof ev.detail.level === 'number', 'bad');
  console.log(ev.detail.level);
});

@ferdodo
Copy link

ferdodo commented Apr 9, 2022

This issue is still relevant, there are only workarounds in this thread to achieve this:

Different means to the same end:

element.addEventListener('myCustomEvent', (event: Event) => {
    const detail = (event as CustomEvent).detail;
});

@citkane
Copy link

citkane commented Aug 17, 2022

I found this thread while intending to file a new bug report.
Agreed @ferdodo , this thread is still relevant and has only workarounds but no recognition of this as a bug since 2018.

Would an admin maybe tag this thread as a bug?
Should I create a new issue tagged as a bug?

Here is the problem illustrated on the typescript playground. Typescript v4.7.4

It is persisting into 4.8Beta and Nightly.

@LukasBombach
Copy link

LukasBombach commented Sep 28, 2022

I regard this as a bug. MDN states that you can dispatch a CustomEvents to EventTargets, yet TypeScript does not recognize this. Compare EventTarget on MDN

This works on all modern browsers.

The interface is typed as

lib.dom.ts

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?

@ChristophP
Copy link

I'm seeing this issue as well. This bug seems to have existed for quite a while now.

@sejori
Copy link

sejori commented Jan 19, 2023

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 CustomEvents and then apply the listener function to ancestor nodes in a tree of event targets:

  addBranchEventListener(type: string, listener: (e: CustomEvent) => void) {
    this.addEventListener(type, (e) => listener(e as CustomEvent))

    if (this.parent) this.parent.addBranchEventListener(type, listener)
  }

It would be nice to not need the type assertion here.

@saschanaz
Copy link
Contributor

saschanaz commented Jan 19, 2023

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

@mkcode
Copy link

mkcode commented Apr 1, 2023

Chiming in to say that this bug is still at large!

@LukasBombach describes how this should be fixed: #28357 (comment)

I no longer thing this a bug. See here

@mkcode
Copy link

mkcode commented Apr 1, 2023

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

@saschanaz pointed out that these methods should be used

@wesbos
Copy link

wesbos commented Apr 24, 2023

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
});

@panoply
Copy link

panoply commented May 24, 2023

Adding this here for the sake of brevity.

Similar to how you'd extend the Window (globalThis) object, the same logic can be applied here via the WindowEventMap within a definition file:

interface Example {
   foo: string;
   bar: string;
}

declare global {
    interface WindowEventMap {
      'custom:event-1': CustomEvent<Example>;
      'custom:event-2': CustomEvent<Example>;
    }
}

@mp3por
Copy link

mp3por commented Mar 20, 2024

FYI this is how I did it without any additional changes - I just make sure I type the handler correctly and it works

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: lib.d.ts The issue relates to the different libraries shipped with TypeScript Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

Successfully merging a pull request may close this issue.