Argument of type '(event: BlahEvent) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject'

Viewed 5374

I've read and tried suggestions found in Argument of type '(e: CustomEvent) => void' is not assignable to parameter of type 'EventListenerOrEventListenerObject' but haven't been able to sensibly apply them to my situation

I've taken over a project that was TS2.0 and I'm looking to move to 3.1 with it. I've read around regarding the error in the title and sort of understand why it occurs, but my typescript knowledge is limited at this point

I have these classes:

EventTargetImpl seems to be a class for managing a collection of event listeners

class EventTargetImpl implements EventTarget {
    private eventListeners: any = {};

    public addEventListener(type: string, listener: EventListenerOrEventListenerObject, useCapture?: boolean): void {
        if (!this.eventListeners.hasOwnProperty(type))
            this.eventListeners[type] = [];
        this.eventListeners[type].push(listener);
    }

    public dispatchEvent(event: Event): boolean {
        var type = event.type;
        if (!this.eventListeners.hasOwnProperty(type)) {
            this.eventListeners[type] = [];
        }
        for (var i = 0; i < this.eventListeners[type].length; i++) {
            this.eventListeners[type][i].call(this, event);
        }
        return true;
    }

    public removeEventListener(type: string, listener: EventListenerOrEventListenerObject, useCapture?: boolean): void {
        if (!this.eventListeners.hasOwnProperty(type))
            return;
        var listenerIndex = this.eventListeners[type].indexOf(listener);
        if (listenerIndex === -1)
            return;
        this.eventListeners[type].splice(listenerIndex, 1);
    }
}

InputEvent class defines constant strings of event names that are some kind of local version of events.

class InputEvent extends EventImpl
{
    public static TYPE_UP = "up";
    constructor(type: string) {
        super(type);
    }
}

..but other than that doesn't seem to do much other than extend EventImpl:

class EventImpl implements Event
{
    public bubbles: boolean;
    public cancelBubble: boolean;
    public cancelable: boolean;
    public currentTarget: EventTarget;
    public defaultPrevented: boolean;
    public eventPhase: number;
    public isTrusted: boolean;
    public returnValue: boolean;
    public srcElement: Element;
    public target: EventTarget;
    public timeStamp: number;
    public type: string;
    public initEvent(eventTypeArg: string, canBubbleArg: boolean, cancelableArg: boolean): void { throw "not implemented";}
    public preventDefault(): void {
        this.defaultPrevented = true;
    }
    public stopImmediatePropagation(): void { throw "not implemented"; }
    public stopPropagation(): void { throw "not implemented";}
    public AT_TARGET: number;
    public BUBBLING_PHASE: number;
    public CAPTURING_PHASE: number;
    constructor(type: string) {
        this.type = type;
    }
}

InputWrapper. This class seems to have/hold an HTML element and either registers standard handlers for the element's events, or it replaces them with handlers attached to itself, of the local type events named in InputEvent

class InputWrapper extends EventTargetImpl {

    constructor(private element: HTMLElement) {
        super();
        this.attachListeners();
    }

    private attachListeners(): void {
        var detachGlobalListeners: () => void;
        var attachGlobalListeners: () => void;

        var mouseUpEventHandler = (event) => {
            var point = this.normaliseMouseEventCoords(event);
            this.handleMouseUp(point.x, point.y, event, InputType.Mouse);
            detachGlobalListeners();
        };

        this.element.addEventListener("mouseup", (event) => {
            if (!this.globalMode) {
                this.handleMouseUp(event.offsetX, event.offsetY, event, InputType.Mouse);
            }
        });

        detachGlobalListeners = () => {
            this.globalMode = false;
            window.removeEventListener("mouseup", mouseUpEventHandler);
        };

        attachGlobalListeners = () => {
            this.globalMode = true;
            window.addEventListener("mouseup", mouseUpEventHandler);
        };
    }

    private handleMouseUp(x: number, y: number, event: Event, inputType: InputType): void {
        event.stopPropagation();
        event.preventDefault();
        this.dispatchEvent(new InputEvent(InputEvent.TYPE_UP, { x: x, y: y }, inputType));
    }
}

Here's where it's used and also where I get the error. I get a similar error on the dispatchEvent call in the lines of code just above too:

        var inputWrapper = new InputWrapper(this.foreground);
        inputWrapper.addEventListener(InputEvent.TYPE_UP, (event: InputEvent) => { 
          this.handleMouseUp(event.coords.x, event.coords.y); 
        });

I linked an SO answer I've already taken a look at; I like jcalz's suggestion that I can declaration-merge in my local events as being of type InputEvent and then use a strongly typed overload of addEventHandler, rather than a stringly typed one, but I can't work out where/how to do this in the object/inheritance hierarchy I have

I also tried FrederikKrautwald's suggestion of tacking as (e: Event) => void) onto the end of the inline function but TypeScript changes to complaining

Conversion of type '(event: InputEvent) => void' to type '(e: Event) => void' may be a mistake because neither type sufficiently overlaps with the other

A modified form of JacobEvelyn's suggestion is most like I'd do it in C#, if I had a specific type boxed in a generic one:

inputWrapper.addEventListener(InputEvent.TYPE_UP, (event: Event) => {
   let iv: InputEvent = (event as InputEvent); 
   this.handleMouseUp(iv.coords.x, iv.coords.y); 
});

But again I get the "neither type sufficiently overlaps with the other" error

1 Answers

How to resolve the "neither type sufficiently overlaps the other" error

A preliminary warning: type assertions are not type safe, in general. The compiler often does not let you do things because it is a mistake for you to do them. However, there are also many situations in which you are sure something is safe but the compiler isn't convinced. And you can use type assertions to force the compiler to accept your version of the truth. This is all well and good unless you happen to be wrong, in which case you can expect crazy runtime behavior and you shouldn't blame the poor compiler that tried to warn you. The compiler isn't as clever as a human being, though, so assertions are often necessary. From here on out I assume that the assertion you want to make is safe enough.

Here is a general example that gives the error you've encountered.

function assert<T, U>(t: T): U {
  return t as U; // error
  // [ts] Conversion of type 'T' to type 'U' may be a mistake because 
  // neither type sufficiently overlaps with the other. If this was 
  // intentional, convert the expression to 'unknown' first.
}

We have tried to assert that a value of type T is of type U, and we get the "neither type sufficiently overlaps" error. Basically type assertions can be used to widen a type to a supertype, which is always safe since you're being more general (every dog can be referred to correctly as an animal); and they can also be used to narrow a type to a more specific type, which is unsafe since you're being more specific (not every animal can be referred to correctly as a dog)... this lack of type safety is intentional and the reason why we have assertions in the first place. But in TypeScript type assertions cannot be used to directly convert from one type to an unrelated type which is neither narrower nor wider (so I can directly assert that a cat is an animal, and I can directly assert that an animal is a dog, but I can't directly assert that a cat is a dog).

The key to resolving this is also mentioned in the error message: "convert the expression to unknown first":

function assert<T, U>(t: T): U {
  return t as unknown as U; // works
}

The error has gone away because we have widened t from type T all the way up to the top type unknown, and then narrowed from unknown down to type U. Each of those steps is allowed, but you can't skip the middle step. (I can't say "this cat is a dog" but I can say "this cat is an animal which is a dog".)

There are other possible intermediate types you can use; it just has to be a common supertype or common subtype of the two unrelated type:

  return t as unknown as U; // okay, widen T to top type unknown, and narrow unknown to U
  return t as (T | U) as U; // okay, widen T to (T | U), and narrow (T | U) to U

  return t as never as U; // okay, narrow T to bottom type never, and widen never to U
  return t as (T & U) as U; // okay, narrow T to (T & U), and widen (T & U) to U

  return t as any as U; // okay, any is both top-like and bottom-like, 
  // so you are, uh, "narrowidening" it through any 

This middleman assertion is a fairly common practice (especially t as any as U, since unknown was only introduced in TypeScript 3.0), so don't feel too bad about using it. The cumbersome nature is primarily there to force you to think about what you're doing: are you sure that this value can be treated as this type? If not, some other solution would be better than type assertion. If so, then carry on!

Hope that helps; good luck!

Related