Search code examples
typescriptes6-mapkeyof

define Map with T[K]


I'm currently in the process of transitioning from JS to TS and can't quite get my head around keyof. I found https://github.com/kimamula/TypeScript-definition-of-EventEmitter-with-keyof and now I'm trying to implement this. All went fine until I turned on 'noImplicitAny'.

I have made an example in the ts playground to show the issue I'm having:

In the constructor there's no access to K so I had to explicitly make the Map (arg: any) to get it to work.

Is there a solution that avoids using any when creating the Map?


Solution

  • The problem is that you are trying to assign (arg: keyof T) => any to (arg: T[K]) => any. keyof T represents any property name of T For example in this class:

    class X { test: string; test2: number; }
    

    keyof X is the same as 'test' | 'test2' while T[K] with K extends keyof T represents the type of the property of T with the name K, so for the class above T[K] can be either number or string depending on the value passed for K

    So if you have Emitter<X> you would basically trying to assign (arg: string | number) => any to (arg: 'test' | 'test2') => any which is definitely not type safe.

    My guess is that you want to constrain listener to have an argument with the same property type as a member of the class. For that you should use Map<string, ((arg: T[keyof T]) => any) []> to have arg constrained to any possible property type of T

    class EventEmitter<T> {
    
      private events: Map<string, ((arg: T[keyof T]) => any) []>;
    
      constructor() {
        this.events = new Map<string, ((arg: T[keyof T]) => any) []>();
      }
    
      public getOrCreateEvents<K extends keyof T>(event: K, listener: (arg: T[K]) => any): ((arg: T[K]) => any) [] {
    
        return this.events.get(event) || this.events.set(event, new Array<(arg: T[K]) => any>()).get(event);
      }
    }
    
    // Test 
    class X { test: string; test2: number; }
    var d = new EventEmitter<X>();
    d.getOrCreateEvents("test2", c => console.log(c));// c is infered to string
    d.getOrCreateEvents("test2", c => console.log(c)); // c is infered to number