skip to Main Content

Let’s say I have this generic class interface:

export interface Base<E extends { [key: string | symbol]: () => void } = {}> extends EventEmitter {
  Events: Record<string, keyof E>;

  on(event: keyof E, cb: E[keyof E]): this;
  once(event: keyof E, cb: E[keyof E]): this;
  off(event: keyof E, cb: E[keyof E]): this;
  emit(event: keyof E, ...args: Parameters<E[keyof E]>): boolean;
}

The idea here is to be able to define emitable events together with expected callback signature like this:

export enum HistoryEvents {
  History = 'history',
}

interface HistoryEventsI {
  [HistoryEvents.History]: (history: any) => void;
}

export interface History extends Base<HistoryEventsI> {
  // ...
}

However I’m getting this error with such code:

Type HistoryEventsI does not satisfy the constraint
{
  [key: string]: () => void;
  [key: symbol]: () => void;
}
Index signature for type string is missing in type HistoryEvents

What am I doing wrong?

2

Answers


  1. You have run into one of the differences b/w a type and interface.

    The reason it is not working is because HistoryEventsI is not assignable to { [key: string | symbol]: () => void }.

    The below works (Have removed the extra argument):

    interface EventEmitter {
    
    }
    
    export interface Base<E extends { [key: string | symbol]: () => void } = {}> extends EventEmitter {
      Events: Record<string, keyof E>;
    
      on(event: keyof E, cb: E[keyof E]): this;
      once(event: keyof E, cb: E[keyof E]): this;
      off(event: keyof E, cb: E[keyof E]): this;
      emit(event: keyof E, ...args: Parameters<E[keyof E]>): boolean;
    }
    
    export enum HistoryEvents {
      History = 'history',
    }
    
    type HistoryEventsT = {
      [HistoryEvents.History]: () => void;
    }
    
    interface HistoryEventsI  {
      [HistoryEvents.History]: () => void;
    }
    
    export interface History extends Base<HistoryEventsI> {
      // ...
    }
    
    export interface History extends Base<HistoryEventsT> {
      // ...
    }
    
    type X = { [key: string | symbol]: () => void };
    
    type AlwaysFalse = HistoryEventsI extends X ? true : false;
    
    type AlwaysTrue = HistoryEventsT extends X ? true : false;
    
    interface HistoryEventsI  {
      [HistoryEvents.History]: () => void;
      newPropertyHere : () => void;
    }
    

    HistoryEventsI does not extend X but HistoryEventsT does.

    There is an issue on the repo discussing the same.

    Basic idea is that it is not sure that the interface will always have a string/symbol as a key. New properties can be added to interfaces.

    Here is one of the most important replies from the above thread:

    Just to fill people in, this behavior is currently by design. Because interfaces can be augmented by additional declarations but type aliases can’t, it’s "safer" (heavy quotes on that one) to infer an implicit index signature for type aliases than for interfaces. But we’ll consider doing it for interfaces as well if that seems to make sense

    Playground for the workaround with types.


    While it is true for JS objects the keys can only be string/symbol, but imagine you had another type which was like { a: 2, b: 2}. Playground for reference

    Login or Signup to reply.
  2. First thing you should update in your code is to update object mapping in interface Base:

    export interface Base<E extends { [key in string | symbol]: (...args: any[]) => void } = {}> extends EventEmitter {...}
    

    This updated code adds contraits to key and values of the generic parameter E, where each key must be string or symbol and values are functions that accept any amount of arguments with any type ...args: [].

    And secondly, I suggest you use a type keyword instead of interface keyword to define HistoryEventsI because generic parameter of Base interface accepts an object. Using interface might cause some unexpected, unnecessary errors:

    type HistoryEventsI = {
      [HistoryEvents.History]: (history: any) => void;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search