27
votes

I have this custom event setup, and it works with TypeScript 2.5.3, but when I updated to 2.6.1 I get an error

window.addEventListener('OnRewards', (e: CustomEvent) => {
    // my code here
})

[ts] Argument of type '(e: CustomEvent) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject'.

Type '(e: CustomEvent) => void' is not assignable to type 'EventListenerObject'.

Property 'handleEvent' is missing in type '(e: CustomEvent) => void'.

I am not exactly sure what to do here to fix this.

4
What is CustomEvent? Maybe try with e: Event?Explosion Pills
CustomEvent is var event = new CustomEvent('OnRewards', { detail: data });Get Off My Lawn
If I use Event then it doesn't know what detail is inside of the functionGet Off My Lawn
Maybe they introduced a new bug in the latest version?Get Off My Lawn

4 Answers

33
votes

This is due to the behavior of the --strictFunctionTypes compiler flag added in TypeScript v2.6. A function of type (e: CustomEvent) => void is no longer considered to be a valid instance of EventListener, which takes an Event parameter, not a CustomEvent.

So one way to fix it is to turn off --strictFunctionTypes.


Another way is to pass in a function that takes an Event and then narrows to CustomEvent via a type guard:

function isCustomEvent(event: Event): event is CustomEvent {
  return 'detail' in event;
}

window.addEventListener('OnRewards', (e: Event) => {
  if (!isCustomEvent(e))
    throw new Error('not a custom event');
  // e is now narrowed to CustomEvent ...
  // my code here 
})

A third way is to use the other overload of addEventListener():

addEventListener<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, useCapture?: boolean): void;

If the type parameter is the name of a known event type (K extends keyof WindowEventMap) like "onclick", then the listener function will expect its parameter to be of that narrowed event type (WindowEventMap[K]). The problem is that "OnRewards" is not a known event type... unless you use declaration merging to make it known:

// merge into WindowEventMap
interface WindowEventMap {
    OnRewards: CustomEvent
}

Or, if you're inside a module (anything with export in it), use global augmentation:

// merge into WindowEventMap
declare global {
  interface WindowEventMap {
    OnRewards: CustomEvent
  }
}

Then use your code as before:

// no error!
window.addEventListener('OnRewards', (e: CustomEvent) => {
    // my code here
})

So, those are your options. Which one you want to choose is up to you. Hope that helps; good luck!

8
votes

Building off of jcalz's excellent answer, you can also use a type assertion to be a little cleaner:

window.addEventListener('OnRewards', (e: Event) => {
    const detail = (<CustomEvent>e).detail;
    ...
});
3
votes

You also have this option:

window.addEventListener('OnRewards', (e: CustomEvent) => {
    // your code here
} as (e: Event) => void)
0
votes

I created a generic function based off @jcalz's answer

/**
 * Checks whether an object can be safely cast to its child type
 * @param parent the object to be 'narrowly' cast down to its child type
 * @param checkForProps props which aught to be present on the child type
 */
export function isSubTypeWithProps<P, C extends P>(parent: P, ...checkForProps: (keyof C)[]): parent is C {
  return checkForProps.every(prop => prop in parent);
}

/**
 * Usage example
 */
const el = document.getElementById('test');
el.addEventListener('click', (e: Event) => {
  if (isSubTypeWithProps<Event, MouseEvent>(e, 'which')) {
    if (e.which === 1) { // primary mouse button only ('which' prop is only available on MouseEvent)
      console.log('clicked');
    }
  }
});