TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

TypeScript doesn't allow event : CustomEvent in addEventListener

Open mmakrzem opened this issue 7 years ago • 44 comments

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

}

mmakrzem avatar Nov 04 '18 01:11 mmakrzem

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?

mjbvz avatar Nov 06 '18 00:11 mjbvz

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

C:\Users\me\Documents\work>npm list typescript
[email protected] C:\Users\me\Documents\work
`-- @stencil/[email protected]
  `-- [email protected]

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 avatar Nov 06 '18 02:11 mmakrzem

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"
  },

mmakrzem avatar Nov 06 '18 02:11 mmakrzem

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

msheakoski avatar Nov 07 '18 02:11 msheakoski

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

saschanaz avatar Nov 08 '18 10:11 saschanaz

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

essenmitsosse avatar Feb 06 '19 07:02 essenmitsosse

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

mmakrzem avatar Feb 06 '19 11:02 mmakrzem

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!

yashsway avatar Apr 10 '19 19:04 yashsway

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

Nikohelie avatar Jul 29 '19 13:07 Nikohelie

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)

revmischa avatar Aug 01 '19 15:08 revmischa

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

jamie-pate avatar Sep 16 '19 18:09 jamie-pate

@ 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

mjbvz avatar Nov 06 '19 00:11 mjbvz

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 avatar Jan 23 '20 04:01 TrejGun

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

TrejGun avatar Jan 23 '20 04:01 TrejGun

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.

jorge-ui avatar Mar 06 '20 09:03 jorge-ui

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 :(

leviwheatcroft avatar Mar 14 '20 06:03 leviwheatcroft

Different means to the same end:

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

jasonhulbert avatar Mar 20 '20 16:03 jasonhulbert

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

Tundon avatar Jul 27 '20 06:07 Tundon

Just got bitten by this. It's been almost 2 years since this issue was open. I'm guessing this is some hard to solve limitation but still, an official response from the Typescript devs would be very nice.

Meanwhile here is a way to do it without type assertion. The addEventListener method of the EventTarget interface supports two types as listener parameter. The first and more elegant is an EventListener but as stated in this discussion it requires a type assertion for the compiler to accept it when using a CustomEvent instead of an Event as parameter. The second allowed type is an EventListenerObject which actually does work as expected without type assertions. Here is an example of the two possible options.

const eventTarget = new EventTarget()
const e = new CustomEvent('test', {detail: 'Just a test!'})

// 1 Using an Object that implements the EventListenerObject interface, no type assertion required
eventTarget.addEventListener('test', {
    handleEvent(e: CustomEvent) {
        console.log(e.detail)
    }
})

// 2 Using a function with the EventListener signature, type assertion is required
eventTarget.addEventListener('test', ((e: CustomEvent) => {
    console.log(e.detail)
}) as EventListener)

// Without type assertion you get an error
// eventTarget.addEventListener('test', (e: CustomEvent) => {
//     console.log(e.detail)
// })

eventTarget.dispatchEvent(e)

Here is a link to the playground to see it in action.

carragom avatar Oct 18 '20 07:10 carragom

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

Then, anywhere in your project... Screenshot 2020-10-18 160638

Screenshot 2020-10-18 154234

Screenshot 2020-10-18 160124

Enjoy everyone! :)

jorge-ui avatar Oct 18 '20 20:10 jorge-ui

@carragom Playground

jorge-ui avatar Oct 18 '20 20:10 jorge-ui

Thanks a lot !!! Your solution is definitively cleaner. Sadly it does not appear to work with EventTarget, it requires Window. Here is a modified version of my playground incorporating your proposal. Am I missing something?

carragom avatar Oct 20 '20 20:10 carragom

I am also waiting for the dom-lib support for this. Meanwhile I found extending WindowEventMap not to be the best solution if you use DOM Events too often, as I do. So how I fixed it for my use was to define an alternative addEventListener

type CustomEventHandler<T> = (event: CustomEvent<T>) => void

declare global {
	interface HTMLElement {
		addEventListener<T>(type: string, listener: CustomEventHandler<T>, options?: boolean | AddEventListenerOptions): void
	}
}

image

a11delavar avatar Nov 12 '20 14:11 a11delavar

Just add generic type param any to addEventListener to make the error go away:

window.addEventListener<any>('someCustomEvent', (event: CustomEvent<string>) => {
    console.log('No errors and fully typed', event.detail)
})

aleksre avatar Nov 17 '20 14:11 aleksre

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.

hrsh7th avatar Dec 17 '20 10:12 hrsh7th

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.

chuanqisun avatar Dec 20 '20 01:12 chuanqisun

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.

michaeljaltamirano avatar Mar 03 '21 03:03 michaeljaltamirano

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?

carragom avatar Apr 12 '21 19:04 carragom

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

pietrovismara avatar Feb 22 '22 16:02 pietrovismara

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 avatar Mar 18 '22 00:03 greggman