1
votes

I am trying to use extends in a generic parameter to index an object but I'm getting the following error:

Argument of type 'IHandlerMap[K]' is not assignable to parameter of type 'BasicHandler'. Type 'BasicHandler | KeyPressHandler' is not assignable to type 'BasicHandler'. Type 'KeyPressHandler' is not assignable to type 'BasicHandler'.ts(2345)

Code:

export type BasicHandler = () => void;
export type KeyPressHandler = (keyCode: number) => void;

interface IHandlerMap {
  OnTick: BasicHandler;
  OnKeyDown: KeyPressHandler;
}

type HandlersType = {[key in keyof IHandlerMap]: IHandlerMap[key][]};

export class EventManager {
  private readonly handlers: HandlersType = {
    OnTick: [],
    OnKeyDown: [],
  };

  public addHandler<K extends keyof IHandlerMap>(
    type: K,
    handler: IHandlerMap[K]
  ): {
    this.handlers[type].push(handler); // Error is occuring here.
  }
}

It appears that TS is only able to deduce that the object property is an array of BasicHandler types when it can be BasicHandler OR KeyPressHandler?

How do I use keyof to index an interface (handler: IHandlerMap[K]) and then use that parameter to push to a object key that is also type'd (this.handlers[type].push(handler)?

I'm having a lot of trouble explaining what I'm trying to do, is this the wrong way to go about doing this? If so, what is an alternative?

1
Interesting, I dont see any other way than assertion to unsound type like any for now. I hope @jcalz will check it out. - Maciej Sikora

1 Answers

1
votes

My guess is, the expression type of this.handlers[type] triggers your error:

Inside the addHandler function body we don't know, which of the two values "OnTick" or "OnKeyDown" property type has, so its base type keyof IHandlerMap or "OnTick" | "OnKeyDown" is assumed for property access. The Property access this.handlers[type] will resolve to the type HandlersType[keyof IHandlerMap], which is BasicHandler[] | KeyPressHandler[].

Now when we invoke this.handlers[type].push, the push method can be either called with BasicHandler[] or KeyPressHandler[], so its type is the union of both push methods:

| (...items: BasicHandler[]): number 
| (...items: KeyPressHandler[]): number

Starting with TS 3.3 you can actually call a union type of functions, but their parameters get intersected (&). The push signature looks rather like this:

(method) Array<T>.push(items: (BasicHandler & KeyPressHandler)[]): number

BasicHandler has no function parameters, KeyPressHandler one. So the intersection results in ... no expected parameters. You can add an argument like s: string to BasicHandler and it gets more apparent (hover over push then).

What can we do?

Use an extra variable for this.handlers with type HandlersTypeMixed or use it for this.handlers directly. Note: (BasicHandler | KeyPressHandler)[] !== BasicHandler[] | KeyPressHandler[]. Only the former won't result in union function types and allow the push. Playground

type HandlersTypeMixed = { [key in keyof IHandlerMap]: (BasicHandler | KeyPressHandler)[] }

public addHandler<K extends keyof IHandlerMap>(
    type: K,
    handler: IHandlerMap[K]
) {
    const hs: HandlersTypeMixed  = this.handlers
    hs[type].push(handler);
}