andywer / typed-emitter

🔩 Type-safe event emitter interface for TypeScript
MIT License
268 stars 24 forks source link

Variable Event Names #15

Open leumasme opened 3 years ago

leumasme commented 3 years ago

Hey there! Is there any way to type events with variable event names? Specifically, i am trying to type https://github.com/PrismarineJS/prismarine-world/blob/55a6f78b4802519b7bee7ace20daa7e956e6ca7f/src/worldsync.js#L69 The World Class emits events called blockUpdate:${position}, which stringifies to blockUpdate:(123, 123, 123). Is typing this kind of event possible with typed-emitter? I know that such complex string type matching is possible in Typescript, see Typescript DFA, but is there any way to do this while using typed-emitter? Thanks for your time!

andywer commented 3 years ago

Hi @leumasme!

Have you tried something like this yet?

import { EventEmitter } from "events"
import TypedEmitter from "typed-emitter"

type MessageEvents = {
  `foo:(${string},${string},${string})`: (payload: number) => void
}

const emitter = new EventEmitter() as TypedEmitter<MessageEvents>

emitter.emit("foo:(1,2,3)", 1)

Unfortunately I could not easily test that and share it, as codesandbox doesn't seem to do plain basic node + TypeScript sandboxes with latest TypeScript versions. Maybe you have an idea where to find something like that?

leumasme commented 3 years ago

Hey @andywer I tried using a template literal as you suggested, and this does not work. VSCode complains about Property or signature expected.ts(1131) image I believe that this wont be possible without some explicit support from the library. Alternatively, another Typed Emitter class that allows string as a fallback type so that explicitly declared event names will be autocompleted, but undeclared ones will still be valid might be an idea? Honestly not sure as i'm not great at typescript myself, just fan-making some types for an existing project.

andywer commented 3 years ago

You definitely have to use a type alias as I did above, not an interface: type MessageEvents = { /* ... */ }

wont be possible without some explicit support from the library

The library supports strict event names. Actually that's more or less the library's whole purpose… 😅 We just have to figure out how to feed that information into TypeScript in a way that it is able to digest it 😉

leumasme commented 3 years ago

You definitely have to use a type alias as I did above, not an interface

Whoops, my bad. Same TS warning though image Sorry if I'm being stupid, not great at typescript as said <-<

andywer commented 3 years ago

I forgot something, too: The computed property name must be written in brackets ([`foo:…`]: …)

Anyhow, TypeScript will still fail to compile and tell us that this is not supported…

image

So it's not possible right now. However the limitation is rooted in the way event types are passed to typed-emitter at the moment as the feature that would be needed is supported by TypeScript, but just not for property names.

I will have to go think if I can come up with an alternative API that allows for template literal typed event names.

andywer commented 3 years ago

I think I've got something… See here.

I tried to seize the opportunity to turn the syntax into something that's hopefully easier to read than the old syntax, too:

type MessageEvents = 
  | EmitterEvent<`foo:${string}`, (size: number, strict: boolean) => void>
  | EmitterEvent<`bar:${string}`, (force: boolean) => void>

const messageEmitter = new EventEmitter() as TypedEmitter<MessageEvents>

messageEmitter.emit("foo:1", 2, false)
andywer commented 3 years ago

ping @leumasme

spencercap commented 2 years ago

@andywer thanks for hackin on this! i find your proposed solution interesting but after forking your example, i found that i was able to emit the foo:1 event with the arguments of the handler function for bar:1. see this for reference of below:

messageEmitter.emit("foo:1", false) // this should not work...

personally, i like the current syntax and would prefer your other solution of putting the dynamic event name in brackets as the key of an interface. like: [`foo:${string}`]: () => void do you think it'd be possible to make this way work?