Skip to content

How to Implement an Event Bus in TypeScript

In real-world software development, design patterns offer reusable solutions and, most prominently, make code easier to maintain.

The Event Bus idea is generally associated with the Publish-subscribe pattern:

Publish–subscribe is a messaging pattern through which message senders, called "publishers", do not program the messages to be sent directly to specific receivers, called "subscribers". Instead, they classify published messages without knowledge of which subscribers sent them.

In other words, an Event Bus can be considered as a global way to transport messages or events to make them accessible from any place within the application.

Event Bus

In this blog post, we'll use TypeScript to implement a general-purpose Event Bus for JavaScript applications.


The concept of an Event Bus comes from the Bus Topology or Bus Network, in which nodes are directly connected to a common half-duplex link called a "bus". Every station will receive all network traffic as seen in the image below.

Bus Topology Network

In a software context, you can assume an object instead of a computer. Then, a message can be triggered from any object through the Bus, and it can be sent using an event, even including some data.

Let's move forward with a TypeScript implementation of this pattern.

Project Setup


You'll need to have installed the following tools in your local environment:

  • Node.js. Preferably the latest LTS version.
  • A package manager. You can use either NPM or Yarn. This tutorial will use NPM.

Initialize the Project

Let's create the project from scratch. First, create a new folder using event-bus-typescript as the name:

mkdir event-bus-typescript

Next, let's initialize the project using NPM, and basic TypeScript configurations:

npm init -y
tsc --init

Update the scripts section from the package.json file:

  "scripts": {
      "start": "tsc && node dist/main.js"

Proceed to update the tsconfig.json file with the configurations listed below.

  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true

Finally, create the src folder to contain the source code files.

Project Structure

The previous project already contains the main configurations ready to go with TypeScript. Open it in your favorite code editor, and make sure to have the following project structure:

|- event-bus-typescript
    |- src/
    |- package.json
    |- tsconfig.json

Event Bus Implementation

For the Event Bus implementation, let's create a src/event-bus/event-bus.ts file.

Creating the Model

To ensure robust typing within the TypeScript context, let's make sure to define a couple of Interfaces for the data model.

// event-bus.ts

export interface Registry {
  unregister: () => void;

export interface Callable {
  [key: string]: Function;

export interface Subscriber {
  [key: string]: Callable;

export interface IEventBus {
  dispatch<T>(event: string, arg?: T): void;
  register(event: string, callback: Function): Registry;

Pay attention to the IEventBus interface, which represents a contract for the future Event Bus class implementation.

// event-bus.ts
export class EventBus implements IEventBus {
  private subscribers: Subscriber;
  private static nextId = 0;

  constructor() {
    this.subscribers = {};

  public dispatch<T>(event: string, arg?: T): void {
    const subscriber = this.subscribers[event];

    if (subscriber === undefined) {

    Object.keys(subscriber).forEach((key) => subscriber[key](arg));

  public register(event: string, callback: Function): Registry {
    const id = this.getNextId();
    if (!this.subscribers[event]) this.subscribers[event] = {};

    this.subscribers[event][id] = callback;

    return {
      unregister: () => {
        delete this.subscribers[event][id];
        if (Object.keys(this.subscribers[event]).length === 0)
          delete this.subscribers[event];

  private getNextId(): number {
    return EventBus.nextId++;

Pay attention to the public methods:

  • The dispatch function makes use of the TypeScript generics to enable capturing the right type of parameters at the moment the method gets called. An example of its use will be provided at the end of this article.
  • The register function receives an event name and a callback function to be invoked. In the end, it returns a Registry object to enable a way of unregistering the same event.

The Singleton Pattern

Since the Event Bus can be accessed from any place in the application, it's important to have a unique instance. Then, you can implement the Singleton Pattern in the existing class as follows.

export class EventBus {
  private static instance?: EventBus = undefined;

  private constructor() {
    // initialize attributes here.

  public static getInstance(): EventBus {
    if (this.instance === undefined) {
      this.instance = new EventBus();

    return this.instance;

Here are the main points of this Singleton class:

  • A static instance is defined to have the unique reference of an object of this class.
  • The constructor method is private, since creating an object from any place is not allowed.
  • The getInstance() method makes sure to instantiate an object of this class only once.

Using the Event Bus

In order to explain the use of the brand-new Event Bus, we'll need to create a src/main.ts file.

import { EventBus } from './event-bus/event-bus';

EventBus.getInstance().register('hello-world', (name: string) => {
        console.log('Hello ' + name);
        console.log('Hello world');

EventBus.getInstance().dispatch<string>('hello-world', 'Luis');

Once you run npm run start command, you should see the following output:

Hello Luis
Hello world
Hello world

However, we can have full control of the initial subscription too:

const registry = EventBus.getInstance().register('hello-world', (name: string) => {
        console.log('Hello ' + name);
        console.log('Hello world');

EventBus.getInstance().dispatch<string>('hello-world', 'Luis');


Let's explain what's happening now:

  • The first line performs a call to the register method from the Event Bus instance.
  • Then, a reference of the Registry object is available through the registry variable.
  • Later, it's possible to perform the registry.unregister() call to avoid dispatching the last "hello world" call.

Here's the output result of those operations:

Hello Luis
Hello world

Live Demo

Wanna play around with this code? Just open the embedded Stackblitz editor:

Source Code of the Project

Find the complete project in this GitHub repository: event-bus-typescript. Do not forget to give it a star ⭐️ and play around with the code.

Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work. Be ready for more articles about Lit in this blog.

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

Functional Programming in TypeScript using the fp-ts Library: Exploring Task and TaskEither Operators cover image

Functional Programming in TypeScript using the fp-ts Library: Exploring Task and TaskEither Operators

Introduction: Welcome back to our blog series on Functional Programming in TypeScript using the fp-ts library. In the previous three blog posts, we covered essential concepts such as the pipe and flow operators, Option type, and various methods and operators like fold, fromNullable, getOrElse, map, flatten, and chain. In this fourth post, we will delve into the powerful Task and TaskEither operators, understanding their significance, and exploring practical examples to showcase their usefulness. Understanding Task and TaskEither: Before we dive into the examples, let's briefly recap what Task and TaskEither are and why they are valuable in functional programming. Task: In functional programming, a Task represents an asynchronous computation that may produce a value or an error. It allows us to work with asynchronous operations in a pure and composable manner. Tasks are lazy and only start executing when we explicitly run them. They can be thought of as a functional alternative to Promises. Now, let's briefly introduce the Either type and its significance in functional programming since this concept, merged with Task gives us the full power of TaskEither. Either: Either is a type that represents a value that can be one of two possibilities: a value of type Left or a value of type Right. Conventionally, the Left type represents an error or failure case, while the Right type represents a successful result. Using Either, we can explicitly handle and propagate errors in a functional and composable way. Example: Handling Division with Either Suppose we have a function divide that performs a division operation. Instead of throwing an error, we can use Either to handle the potential division by zero scenario. Here's an example: `ts import { Either, left, right } from 'fp-ts/lib/Either'; const divide: (a: number, b: number) => Either = (a, b) => { if (b === 0) { return left('Error: Division by zero'); } return right(a / b); }; const result = divide(10, 2); result.fold( (error) => console.log(Error: ${error}`), (value) => console.log(Result: ${value}`) ); ` In this example, the divide function returns an Either type. If the division is successful, it returns a Right value with the result. If the division by zero occurs, it returns a Left value with an error message. We then use the fold function to handle both cases, printing the appropriate message to the console. TaskEither: TaskEither combines the benefits of both Task and Either. It represents an asynchronous computation that may produce a value or an error, just like Task, but also allows us to handle potential errors using the Either type. This enables us to handle errors in a more explicit and controlled manner. Examples: Let's explore some examples to better understand the practical applications of Task and TaskEither operators. Example 1: Fetching Data from an API Suppose we want to fetch data from an API asynchronously. We can use the Task operator to encapsulate the API call and handle the result using the Task's combinators. In the example below, we define a fetchData` function that returns a Task representing the API call. We then use the `fold` function to handle the success and failure cases of the Task. If the Task succeeds, we return a new Task with the fetched data. If it fails, we return a Task with an error message. Finally, we use the `getOrElse` function to handle the case where the Task returns `None`. `typescript import { pipe } from 'fp-ts/lib/function'; import { Task } from 'fp-ts/lib/Task'; import { fold } from 'fp-ts/lib/TaskEither'; import { getOrElse } from 'fp-ts/lib/Option'; const fetchData: Task = () => fetch(''); const handleData = pipe( fetchData, fold( () => Task.of('Error: Failed to fetch data'), (data) => Task.of(Fetched data: ${data}`) ), getOrElse(() => Task.of('Error: Data not found')) ); handleData().then(console.log); ` Example 2: Performing Computation with Error Handling Let's say we have a function divide` that performs a computation and may throw an error. We can use TaskEither to handle the potential error and perform the computation asynchronously. In the example below, we define a `divideAsync` function that takes two numbers and returns a TaskEither representing the division operation. We use the `tryCatch` function to catch any potential errors thrown by the `divide` function. We then use the `fold` function to handle the success and failure cases of the TaskEither. If the TaskEither succeeds, we return a new TaskEither with the result of the computation. If it fails, we return a TaskEither with an error message. Finally, we use the `map` function to transform the result of the TaskEither. `typescript import { pipe } from 'fp-ts/lib/function'; import { TaskEither, tryCatch } from 'fp-ts/lib/TaskEither'; import { fold } from 'fp-ts/lib/TaskEither'; import { map } from 'fp-ts/lib/TaskEither'; const divide: (a: number, b: number) => number = (a, b) => { if (b === 0) { throw new Error('Division by zero'); } return a / b; }; const divideAsync: (a: number, b: number) => TaskEither = (a, b) => tryCatch(() => divide(a, b), (error) => new Error(String(error))); const handleComputation = pipe( divideAsync(10, 2), fold( (error) => TaskEither.left(Error: ${error.message}`), (result) => TaskEither.right(Result: ${result}`) ), map((result) => Computation: ${result}`) ); handleComputation().then(console.log); ` In the first example, we saw how to fetch data from an API using Task and handle the success and failure cases using fold and getOrElse functions. This allows us to handle different scenarios, such as successful data retrieval or error handling when the data is not available. In the second example, we demonstrated how to perform a computation that may throw an error using TaskEither. We used tryCatch to catch potential errors and fold to handle the success and failure cases. This approach provides a more controlled way of handling errors and performing computations asynchronously. Conclusion: In this blog post, we explored the Task` and `TaskEither` operators in the `fp-ts` library. We learned that Task allows us to work with asynchronous computations in a pure and composable manner, while TaskEither combines the benefits of Task and Either, enabling us to handle potential errors explicitly. By leveraging the concepts we have covered so far, such as pipe, flow, Option, fold, map, flatten, and chain, we can build robust and maintainable functional programs in TypeScript using the fp-ts library. Stay tuned for the next blog post in this series, where we will continue our journey into the world of functional programming....

I Broke My Hand So You Don't Have To (First-Hand Accessibility Insights) cover image

I Broke My Hand So You Don't Have To (First-Hand Accessibility Insights)

We take accessibility quite seriously here at This Dot because we know it's important. Still, throughout my career, I've seen many projects where accessibility was brushed aside for reasons like "our users don't really use keyboard shortcuts" or "we need to ship fast; we can add accessibility later." The truth is, that "later" often means "never." And it turns out, anyone could break their hand, like I did. I broke my dominant hand and spent four weeks in a cast, effectively rendering it useless and forcing me to work left-handed. I must thus apologize for the misleading title; this post should more accurately be dubbed "second-hand" accessibility insights. The Perspective of a Developer Firstly, it's not the end of the world. I adapted quickly to my temporary disability, which was, for the most part, a minor inconvenience. I had to type with one hand, obviously slower than my usual pace, but isn't a significant part of a software engineer's work focused on thinking? Here's what I did and learned: - I moved my mouse to the left and started using it with my left hand. I adapted quickly, but the experience wasn't as smooth as using my right hand. I could perform most tasks, but I needed to be more careful and precise. - Many actions require holding a key while pressing a mouse button (e.g., visiting links from the IDE), which is hard to do with one hand. - This led me to explore trackpad options. Apart from the Apple Magic Trackpad, choices were limited. As a Windows user (I know, sorry), that wasn't an option for me. I settled for a cheap trackpad from Amazon. A lot of tasks became easier; however, the trackpad eventually malfunctioned, sending me back to the mouse. - I don't know a lot of IDE shortcuts. I realized how much I've been relying on a mouse for my work, subconsciously refusing to learn new keyboard shortcuts (I'll be returning my senior engineer license shortly). So I learned a few new ones, which is good, I guess. - Some keyboard shortcuts are hard to press with one hand. If you find yourself in a similar situation, you may need to remap some of them. - Copilot became my best friend, saving me from a lot of slow typing, although I did have to correct and rewrite many of its suggestions. The Perspective of a User As a developer, I was able to get by and figure things out to be able to work effectively. As a user, however, I got to experience the other side of the coin and really feel the accessibility (or lack thereof) on the web. Here are a few insights I gained: - A lot of websites apparently tried_ to implement keyboard navigation, but failed miserably. For example, a big e-commerce website I tried to use to shop for the aforementioned trackpad seemed to work fine with keyboard navigation at first, but once I focused on the search field, I found myself unable to tab out from it. When you make the effort to implement keyboard navigation, please make sure it works properly and it doesn't get broken with new changes. I wholeheartedly recommend having e2e tests (e.g. with Playwright) that verify the keyboard navigation works as expected. - A few websites and web apps I tried to use were completely unusable with the keyboard and were designed to be used with a mouse only. - Some sites had elaborate keyboard navigation, with custom keyboard shortcuts for different functionality. That took some time to figure out, and I reckon it's not as intuitive as the designers thought it would be. Once a user learns the shortcuts, however, it could make their life easier, I suppose. - A lot of interactive elements are much smaller than they should be, making it hard to accurately click on them with your weaker hand. Designers, I beg you, please make your buttons bigger. I once worked on an application that had a "gloves mode" for environments where the operators would be using gloves, and I feel like maybe the size we went with for the "gloves mode" should be the standard everywhere, especially as screens get bigger and bigger. - Misclicking is easy, especially using your weaker hand. Be it a mouse click or just hitting an Enter key on accident. Kudos to all the developers who thought about this and implemented a confirmation dialog or other safety measures to prevent users from accidentally deleting or posting something. I've however encountered a few apps that didn't have any of these, and those made me a bit anxious, to be honest. If this is something you haven't thought about when developing an app, please start doing so, you might save someone a lot of trouble. Some Second-Hand Insights I was only a little bit impaired by being temporarily one-handed and it was honestly a big pain. In this post, I've focused on my anecdotal experience as a developer and a user, covering mostly keyboard navigation and mouse usage. I can only imagine how frustrating it must be for visually impaired users, or users with other disabilities, to use the web. I must confess I haven't always been treating accessibility as a priority, but I've certainly learned my lesson. I will try to make sure all the apps I work on are accessible and inclusive, and I will try to test not only the keyboard navigation, ARIA attributes, and other accessibility features, but also the overall experience of using the app with a screen reader. I hope this post will at least plant a little seed in your head that makes you think about what it feels like to be disabled and what would the experience of a disabled person be like using the app you're working on. Conclusion: The Humbling Realities of Accessibility The past few weeks have been an eye-opening journey for me into the world of accessibility, exposing its importance not just in theory but in palpable, daily experiences. My short-term impairment allowed me to peek into a life where simple tasks aren't so simple, and convenient shortcuts are a maze of complications. It has been a humbling experience, but also an illuminating one. As developers and designers, we often get caught in the rush to innovate and to ship, leaving behind essential elements that make technology inclusive and humane. While my temporary disability was an inconvenience, it's permanent for many others. A broken hand made me realize how broken our approach towards accessibility often is. The key takeaway here isn't just a list of accessibility tips; it's an earnest appeal to empathize with your end-users. "Designing for all" is not a checkbox to tick off before a product launch; it's an ongoing commitment to the understanding that everyone interacts with technology differently. When being empathetic and sincerely thinking about accessibility, you never know whose life you could be making easier. After all, disability isn't a special condition; it's a part of the human condition. And if you still think "Our users don't really use keyboard shortcuts" or "We can add accessibility later," remember that you're not just failing a compliance checklist, you're failing real people....

How to Create Standalone Components in Angular cover image

How to Create Standalone Components in Angular

Angular has become one of the most popular frameworks to build web applications today. One of the key features of the framework is its component-based architecture, which allows best practices like modularity and reusability. Each Angular component consists of a template, a TypeScript class and metadata. In this blog post, we will dive deeper into standalone components, and we will explore the anatomy of an application based on them. For the demo application, we will create a card-like component which can be used to render blog posts in a web application. Prerequisites You'll need to have installed the following tools in your local environment: The latest LTS version of Node.js is recommended. Either NPM or Yarn as a package manager. The Angular CLI tool(Command-line interface for Angular). Initialize the Project Let's create a project from scratch using the Angular CLI tool: `bash ng new demo-angular-standalone-components --routing --prefix corp --style css --skip-tests ` This command will initialize a base project using some configuration options: `--routing`. It will create a routing module. `--prefix corp`. It defines a prefix to be applied to the selectors for created components(`corp` in this case). The default value is `app`. `--style css`. The file extension for the styling files. `--skip-tests`. Disable the generation of testing files for the new project. If you pay attention to the generated files and directories, you'll see an initial project structure including the main application module and component: `txt |- src/ |- app/ |- app.module.ts |- app-routing.module.ts |- app.component.ts ` Creating Standalone Components First, let's create the custom button to be used as part of the Card component later. Run the following command on your terminal: `bash ng generate component button --inline-template --standalone ` It will create the files for the component. The --standalone` option will generate the component as _standalone_. Let's update the button.component.ts` file using the content below. `ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'corp-button', standalone: true, imports: [CommonModule], template: , styleUrls: ['./button.component.css'] }) export class ButtonComponent { } ` Pay attention to this component since it's marked as standalone: true`. Starting with Angular v15: components, directives, and pipes can be marked as standalone by using the flag standalone`. When a class is marked as standalone_, it does not need to be declared as part of an `NgModule`. Otherwise, the Angular compiler will report an error. Also, imports` can be used to reference the dependencies. > The imports property specifies the standalone component's template dependencies — those directives, components, and pipes that can be used within its template. Standalone components can import other standalone components, directives, and pipes as well as existing NgModules. Next, let's create the following components: card-title`, `card-content`, `card-actions` and `card`. This can be done at once using the next commands. `bash ng generate component card-title --inline-template --standalone ng generate component card-content --inline-template --standalone ng generate component card-actions --inline-template --standalone ng generate component card --inline-template --standalone ` On card-title.component.ts` file, update the content as follows: `ts //card-title.component.ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'corp-card-title', standalone: true, imports: [CommonModule], template: , styleUrls: ['./card-title.component.css'] }) export class CardTitleComponent { } ` Next, update the card-content.component.ts` file: `ts //card-content.component.ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'corp-card-content', standalone: true, imports: [CommonModule], template: , styleUrls: ['./card-content.component.css'] }) export class CardContentComponent { } ` The card-actions.component.ts` file should have the content below: `ts // card-actions.component.ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'corp-card-actions', standalone: true, imports: [CommonModule], template: , styleUrls: ['./card-actions.component.css'] }) export class CardActionsComponent { } ` Finally, the card.component.ts` file should be defined as follows: `ts //card.component.ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'corp-card', standalone: true, imports: [CommonModule], template: , styleUrls: ['./card.component.css'] }) export class CardComponent { } ` Using Standalone Components Once all Standalone components are created, we can use them without the need to define an NgModule`. Let's update the app.component.ts` file as follows: `ts // app.component.ts import { Component } from '@angular/core'; import { ButtonComponent } from './button/button.component'; import { CardComponent } from './card/card.component'; import { CardTitleComponent } from './card-title/card-title.component'; import { CardContentComponent } from './card-content/card-content.component'; import { CardActionsComponent } from './card-actions/card-actions.component'; @Component({ selector: 'corp-root', standalone: true, imports: [ ButtonComponent, CardComponent, CardTitleComponent, CardContentComponent, CardActionsComponent ], templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'demo-angular-standalone-components'; } ` Again, this Angular component is set as standalone: true` and the `imports` section specifies the other components created as dependencies. Then, we can update the `app.component.html` file and use all the standalone components. `html Latest Posts Introduction to Angular Angular is a component-based framework for building scalable web applications. View Introduction to TypeScript TypeScript is a strongly typed programming language that builds on JavaScript, providing better tooling at any scale. View ` This may not work yet, since we need to configure the Angular application and get rid of the autogenerated module defined under the app.module.ts` file. Bootstrapping the Application Angular provides a new API to use a standalone component as the application's root component. Let's update the main.ts` file with the following content: `ts // main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent); ` As you can see, we have removed the previous bootstrapModule` call and the `bootstrapApplication` function will render the standalone component as the application's root component. You can find more information about it here. Live Demo and Source Code Want to play around with this code? Just open the Stackblitz editor or the preview mode in fullscreen. Conclusion We’re now ready to use and configure the Angular components as standalone and provide a simplified way to build Angular applications from scratch. Do not forget that you can mark directives and pipes as standalone: true`. If you love Angular as much as we do, you can start building today using one of our kits or by looking at their related showcases for more examples....

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry cover image

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry

In this episode of the engineering leadership series, Rob Ocel interviews David Cramer, co-founder and CTO of Sentry, delving into the importance of decision-making, risk-taking, and the challenges faced in the software engineering industry. David emphasizes the significance of having conviction and being willing to make decisions, even if they turn out to be wrong. He shares his experience of attending a CEO event, where he discovered that decision-making and conflict resolution are struggles even for successful individuals. David highlights the importance of making decisions quickly and accepting the associated risks, rather than attempting to pursue multiple options simultaneously. He believes that being decisive is crucial in the fast-paced software engineering industry. This approach allows for faster progress and adaptation, even if it means occasionally making mistakes along the way. The success of Sentry is attributed to a combination of factors, including market opportunity and the team's principles and conviction. David acknowledges that bold ideas often carry a higher risk of failure, but if they do succeed, the outcome can be incredibly significant. This mindset has contributed to Sentry’s achievements in the industry. The interview also touches on the challenges of developing and defending opinions in the software engineering field. David acknowledges that it can be difficult to navigate differing viewpoints and conflicting ideas. However, he emphasizes the importance of standing by one's convictions and being open to constructive criticism and feedback. Throughout the conversation, David emphasizes the need for engineering leaders to be decisive and take calculated risks. He encourages leaders to trust their instincts and make decisions promptly, even if they are uncertain about the outcome. This approach fosters a culture of innovation and progress within engineering teams. The episode provides valuable insights into the decision-making process and the challenges faced by engineering leaders. It highlights the importance of conviction, risk-taking, and the ability to make decisions quickly in the software engineering industry. David's experiences and perspectives offer valuable lessons for aspiring engineering leaders looking to navigate the complexities of the field....