Search code examples
javascripttypescriptevent-handlingtypescript-generics

Typescript: How can I describe an object key dynamically with generics?


I am trying to describe a dictionary of event handlers (with different payload shapes) like this:

type Events = 'foo' | 'bar';

type EventsPayload = { foo: string, bar: number };
type EventHandler<Event extends Events> = (args: EventsPayload[Event]) => void;

const eventHandlers: { [Event in Events]?: EventHandler<Event> } = {};
export function addEventHandler<Event extends Events>(
  name: Event,
  handler: EventHandler<Event>,
) {
  eventHandlers[name] = handler;
}

This is giving me the error:

Type 'EventHandler' is not assignable to type '{ foo?: EventHandler<"foo"> | undefined; bar?: EventHandler<"bar"> | undefined; }[Event]'.

If I remove the generics when I insert the handler method it works:

eventHandlers.foo = (payload: string) => {};

Solution

  • You can make it work by referring to the type of eventHandlers when defining the type of the handler parameter. (This is what they do for addEventListener in lib.dom.d.ts, for example.) I find it easiest to do that by using a type alias for the type of the eventHandlers object:

    type Events = "foo" | "bar";
    
    type EventsPayload = { foo: string; bar: number };
    type EventHandler<Event extends Events> = (args: EventsPayload[Event]) => void;
    
    // *** Define the type
    type EventHandlers = {
        [Event in Events]?: EventHandler<Event>;
    };
    
    const eventHandlers: EventHandlers = {};
    
    // *** Use it for `handler`
    export function addEventHandler<Event extends Events>(
        name: Event,
        handler: EventHandlers[Event]
    ) {
        eventHandlers[name] = handler;
    }
    

    Playground link

    You don't actually need to have the type alias, though, you could use typeof eventHandlers instead:

    type Events = "foo" | "bar";
    
    type EventsPayload = { foo: string; bar: number };
    type EventHandler<Event extends Events> = (args: EventsPayload[Event]) => void;
    
    const eventHandlers: { [Event in Events]?: EventHandler<Event> } = {};
    export function addEventHandler<Event extends Events>(
        name: Event,
        handler: typeof eventHandlers[Event]
    ) {
        eventHandlers[name] = handler;
    }
    

    Playground link

    Subjectively speaking, to me it's clearer if we have a type alias to work with.