Events are an intuitive way to model the execution flows of applications when several modules, each one with complex lifecycles, need to synchronize with each other. This is especially true if the application involves frequent user-initiated inputs.
The application I maintain at work falls exactly in this space (it is a backend system for managing interactive conversational bots). Yes, you could happily use a package for that (here a good one), but I always prefer to understand how things work under the hood.
For this reason, in this article I’ll present you the implementation of a very simple event system for TypeScript.
Simple Synchronous Signals
At the very basic, an Event is just a simple object to which someone can subscribe and receive a signal when the event is triggered. We can imagine a class with three basic methods:
onto subscribe to the event/signal.
triggerused to trigger the event (and notify the subscribers).
I will call this class
Signal (I will not use
Event frequently name-clash in TypeScript).
Very simple. Does it work? It is easy to check. Let’s create a class
Now, let’s create a class to receive the event. For instance:
And finally let’s run everything:
A slightly improved example
That’s cool. But there is a problem.
Ops. Now the listener can make the dog bark! This is a clear violation of what an event subscriber can do. Luckily, we can easily fix this.
First, let’s create a public interface for our signal. This will only include the methods that can be used by the listeners. No triggers.
Now we can edit our
Signal class. We will implement this class and provide an
And update the event on
Dog in this way:
expose only returns an
ISignal, the listener would be unable to trigger the event.1
EvilDogListner will not compile, and we will get the error:
Property 'trigger' does not exist on type 'ISignal<Dog, string>'.
Simple Asynchronous Signals
Signal we implemented before can handle correctly synchronous events: when the event is triggered, we run the subscribed function, and it immediately returns. Blocking the execution until every event handler is executed. But what if we want to subscribe an
The upgrade is simple. We will change the handler function signature from
(source: S, data: T) => void to
(source: S, data: T) => Promise<void>.
trigger method is now
async and, when called, it will immediately return without waiting for the completion of the several handlers.
In some cases, we may want to trigger an event and wait for all the subscribers to complete like in the synchronous case. For this reason, we will also add a
triggerAwait version of
Asynchronous Signals with Binding
I will now show you a variation of
AsyncSignal that is slightly more robust (depending on the use cases). With the previous version, in fact, it is possible to double-subscribe to an event.
A better solution is to not attach a handler as a function, instead, let’s use an object containing more information on the binding.
We can put there all the info we need to tune the binding as we need. In a simple version, we can just add an identifier for the listener so that we can guarantee to have just one handler for each listener (this is useful if the application you are working with can create a lot of dynamic listeners).
The extended implementation is easy. In the example below I changed
bind/unbind, just to have a clearer distinction, but you can keep
on/off if you prefer. Also, in the implementation I used
lodash to simplify the
contains function, but you can avoid that with some extra code.
I hope this is useful for someone. I know there is probably a better solution for that, but this simple event system is serving me well so far. If you think it can be improved in any way, just let me know!
Of course, they could cast it to
Signal… but this will stop not self-damaging coders. ↩︎