DEV Community

Christian Engel
Christian Engel

Posted on • Edited on • Originally published at parastudios.de

A typed event system for Typescript

I am working on a pet project with distributed services right now.

During the development, I was in need of a classic on/off style event system which is usually quickly written with
a couple lines of code. This time however, I wondered if it would be possible to have the whole event system type guarded
with Typescript types.

Get the code

I stored the whole 58 lines of code in a github gist. Please be aware that I don't take any responsibility if the code
does not work as intented or causes failures somewhere. Use it at your own risk.

How to use it

If you have ever worked with a on/off style event emitter, the usage of this system will be straight forward. I am still going
to explain it shortly.

How a basic on/off style event system works

You may skip to the next headline, if you know about this already :)

For an event system, you usually have points in your code which trigger events and broadcast the occurance of the events without
knowing if someone actually listens for them.

Other parts of your code may subscribe to the existing events and react on them. If you ever worked with events in the DOM of a website,
its exactly this pattern:

// Adding a callback function to "change" events on a text box.
// The callback is called every time when a change event happens.
document.getElementById("myCoolTextbox").addEventListener("change", function(event){
    // ...
});
Enter fullscreen mode Exit fullscreen mode

So there may be infinite subscribers (listeners) to some event and whenever it gets triggered, all subscribers will be notified.

Prepare the types of your events

I designed the event system so you can create a typescript interface to define all your existing events as well as their data payloads.

If we for example want to create a fictional video player app with an event system, we could define the following events:

interface AppEvents {
    load: {
      filename: string;
      format: string;
      totalTimeMs: number;
    },
    update: {
        currentTimeMs: number;
    },
    play: void,
    pause: void,
    stop: void
}
Enter fullscreen mode Exit fullscreen mode

So we have a load event when the user opens a file, an update event which fires continuously while playing back the file
and play, pause and stop events that are triggered upon user interaction.

Creating and using the event system

With our typescript interface at hand defining all events in our system, we can actually create the system in code:

// appEvents.ts
import createEventSystem from "./eventSystem.ts";

interface AppEvents {
    load: {
        filename: string;
        format: string;
        totalTimeMs: number;
    },
    update: {
        currentTimeMs: number;
    },
    play: void,
    pause: void,
    stop: void
}

const eventSystem = createEventSystem<AppEvents>();
export default eventSystem;
Enter fullscreen mode Exit fullscreen mode

Now the system can be used from anywhere in our code. Typescript takes care that only known events can be triggered
and they need to receive exactly the expected payloads.

import appEvents from "./appEvents.ts";
import { loadAndParseAudioFile } from "./audioLoader.ts";

export async function loadFile(filename){
    const {
        filename,
        format,
        totalTimeMs
    } = await loadAndParseAudioFile(filename);

    appEvents.trigger("load", {filename, format, totalTimeMs});
}
Enter fullscreen mode Exit fullscreen mode

On some other part of your application, your code may subscribe to the defined events:

// logger.ts
import appEvents from "./appEvents.ts";

appEvents.on("load", (payload) => {
    console.log(`You opened the file ${payload.filename} with a length of ${payload.totalTimeMs} milliseconds.`);
});
Enter fullscreen mode Exit fullscreen mode

Stop listening to an event

To detach from receiving events, one needs to call the .off() method and pass the exactly same callback function to the
off handler:

function handler(data){
    console.log("An update happened!", data);
}

eventSystem.on("update", handler);

// And somewhere else:
eventSystem.off("update", handler);
Enter fullscreen mode Exit fullscreen mode

Please be aware that it needs to be the same callback function and not another one, with similar code.

THIS WONT WORK!

eventSystem.on("update", (data) => {
    console.log("An update happened!", data);
});

eventSystem.off("update", (data) => {
    console.log("An update happened!", data);
});

// These are TWO separate functions, therefore the "off" call did not work.
Enter fullscreen mode Exit fullscreen mode

Using ENUMs as event names

Since typescript interfaces do not care about the format of field names, you are free to define an ENUM to keep your
existing event names for better discoverability in your IDE:

export enum EventNames {
    LOAD,
    UPDATE,
    PLAY,
    PAUSE,
    STOP
};

interface AppEvents {
    [EventNames.LOAD]: {
        filename: string;
        format: string;
        totalTimeMs: number;
    },
    [EventNames.UPDATE]: {
        currentTimeMs: number;
    },
    [EventNames.PLAY]: void,
    [EventNames.PAUSE]: void,
    [EventNames.STOP]: void
}
Enter fullscreen mode Exit fullscreen mode

This way, you can call your triggers and subscriptions like this:

eventSystem.trigger(EventNames.PLAY);

eventSystem.on(EventNames.UPDATE, ({currentTime}) => {
    console.log(`Current time: ${currentTime}`);
});

-------------

This post was published on my [personal blog](https://parastudios.de/), first. 

You should follow me on dev.to for more tips like this. Click the follow button in the sidebar! 🚀
Enter fullscreen mode Exit fullscreen mode

Top comments (0)