mitt icon indicating copy to clipboard operation
mitt copied to clipboard

[TypeScript] define events interface

Open alonronin opened this issue 2 years ago • 3 comments

Hi,

I want to define an events interface like so:

const events = {
  test: (payload) => {
    console.log(payload);
  },
  another: (payload) => {
    console.log(payload);
  }
} as const;

so the event name is dynamic and can be added later on.

I defined the mitt emitter like so:

type EventName = keyof typeof events;
type Payload = { [key: string]: any };
type Events = { [key in EventName]: Payload };

export const analytics: Emitter<Events> = mitt<Events>();

and now i can call the event function:

analytics.on("*", (type, payload) => {
  const event = events[type];
  event?.(payload);
});

and this is works as expected:

analytics.emit("foo", { test: 1 }); // I get error on foo as it is not define in events object

However I cannot define an interface for events like so:

type EventsList = { [key: EventName]: (payload: Payload) => void };

so i'll get the events list like so:

const events: EventsList = {
  test: (payload) => {
    console.log(payload);
  },
  another: (payload) => {
    console.log(payload);
  }
} as const;

I get a ts error:

image

if i define it like this:

type EventsList = { [key: string]: (payload: Payload) => void };

it doesn't enforce the event name.

here is a codesandbox: https://codesandbox.io/s/aged-microservice-oi72gv?file=/src/index.ts:0-558

I'd appreciate any help, thanks.

alonronin avatar May 17 '22 21:05 alonronin

Hi @alonronin ,

Could you solve your issue?

khus29 avatar Jul 14 '22 05:07 khus29

@alonronin Hey, I stumbled upon this issue while working on my own bus and thought although you probably don't need this anymore it's an interesting exercise.

Were you trying to define the schema in a single place, along with the handler implementation? I don't get the "the event name is dynamic" part, and I find the usage super weird, as you declare all the handlers from the get-go, and infer the event types from the actual functions, so events is not an interface, it's the full implementation if I get it right.

Anyway, I think something like this does what you wanted, and should be easy to adjust to take in an interface type instead of the handlers: https://codesandbox.io/s/cool-leftpad-857rrd?file=/src/index.ts

const events = {
  withPrimitive: (payload: string) => {
    console.log(payload);
  },
  withObject: (payload: { a: boolean }) => {
    console.log(payload);
  }
};

type EventSchema<T extends Record<string, (payload: any) => void>> = {
  [K in keyof T]: Parameters<T[K]>[0];
};
type MittSchema = EventSchema<
  typeof events & {
    all: (payload: any) => any;
  }
>;
export const bus: Emitter<MittSchema> = mitt<MittSchema>();

bus.emit("withPrimitive", "hello"); // pass
bus.emit("withPrimitive", 123); // fail

3rd avatar Jun 13 '23 08:06 3rd

@alonronin also stumbled upon this :) Getting back to the original example, it looks like there are 2 easy ways to solve this:

  1. explicitly type the payload in you events object - TS playground
  2. use the new satisfies keyword available from version 4.9 TS playground

stropho avatar Aug 05 '23 10:08 stropho