TypeScript Event Handling Tips & Techniques

Event handling provides a channel of communication between different parts of an application. There are several techniques for creating and handling events, each with its own advantages and disadvantages.

By Tim Trott | (Java|Type)Script / jQuery | April 2, 2020
1,145 words, estimated reading time 4 minutes.

A common pattern we encounter when writing software is the need for communication between components. Something happens in one component and something else must respond or react to that event.

Events provide a channel of communication between different parts of an application. Event emitters act as broadcasters, emitting events at specified points. Event consumers listen to those events and do something in response. Emitters don't need to know ahead of time what will consume or handle their events. This increases flexibility and decoupling.

Event Property Handlers

A simple technique for event creation and handling is the event property handler. Event property handlers allow consumers to define a method called during an event.

For example, let's say we have a class Timer, which performs some basic timing functions. We want to register a handler that executes when the timer completes. First, we define a property signature, onComplete on the Timer that returns a callback. This will be an optional property.

Next, we'll define a start method that begins a timeout. After the timeout completes, we fire our onComplete event. We check to see if the handler has a definition and if it does, we call it.

typescript
/ Timer.ts
export default class Timer {
  public onComplete?: () => void

  public start(): void {
    setTimeout(() => {
      if (!this.onComplete) return
      this.onComplete()
    }, 7000)
  }
}

Now, in some other parts of our application, we can instantiate the Timer and register a handler for the complete event. We do this by assigning a handler function to the onComplete property of the Timer instance.

typescript
/ consumer.ts
import Timer from './Timer'

const t = new Timer()
t.onComplete = () => {
  console.log('timer completed event')
  / do some stuff
}
t.start();

Passing Event Data

To expose some data in the event handler, adjust the property signature with the expected argument type. Then when calling the method, pass in the data. In this example, the event callback exposes the time of the event firing. The consumer can use or ignore it.

typescript
/ Timer.ts
export default class Timer {
  public onComplete?: (time: number) => void

  public start(): void {
    setTimeout(() => {
      if (!this.onComplete) return
      this.onComplete(Date.now())
    }, 7000)
  }
}

/ consumer.ts
t.onComplete = (time: number) => {
  console.log(time)
  / access event data
}
t.onComplete = () => {
  console.log('no data')
  / the data argument is optional
}

Removing an Event Handler

To remove an event handler, delete the property.

typescript
/ consumer.ts
delete t.onComplete

Event Handler Limitations

Event property handlers are a simple way to create and handle events, but it does have a caveat. Attempting to define additional handlers will overwrite existing handlers. For example, in the following example, the second handler overwrites the first.

typescript
/ consumer.ts
t.onComplete = () => {
  console.log('first handler definition')
  / this won't execute
}
t.onComplete = () => {
  console.log('second handler definition')
  / this will execute
}

Event Listeners with EventTarget

You may be familiar with DOM element event listeners. For example, you may recognize the following bit of code, which attaches an event handler to a button when clicked:

typescript
const btn = document
  .getElementById('some-btn')
  .addEventListener('click', () => {
    / do some stuff
  })

This pattern is available to DOM elements that implement the EventTarget interface. The easiest way to gain access to this interface is through inheritance or composition. In this example, we'll look at inheritance.

Using ES6 classes, we can have our Timer class implement this interface by extending the EventTarget class. Note that because our class extends the EventTarget class, we need to call super() in the constructor.

We'll define the 'complete' event as a property. EventTarget works with the Event interface, so we'll initialize this property as a new Event, passing in the event name.

In the start method, after the timeout completes, we'll fire the 'complete' event using the dispatchEvent method, passing in the _complete property. The dispatchEvent method is available on the EventTarget class and takes a single Event argument.

typescript
/ Timer.ts
export default class Timer extends EventTarget {
  constructor() {
    super()
  }

  private _complete: Event = new Event('complete')

  public start(): void {
    setTimeout(() => {
      this.dispatchEvent(this._complete)
    }, 7000)
  }
}

With our event emitter in place, we can instantiate the Timer and register a handler for the complete event using the addEventListener method. The completeHandler method fires when the 'complete' event fires.

typescript
/ consumer.ts
import Timer from './Timer'

const t = new Timer()
const completeHandler = () => {
  console.log('timer completed event')
  / do some stuff
}
t.addEventListener('complete', completeHandler)
t.start()
Unlike event property handlers, you can attach many event handlers to a single event.

/ consumer.ts
import Timer from './Timer'

const t = new Timer()
const handlerOne = () => {
  console.log('first handler definition')
  / this will execute, too
}
const handlerTwo = () => {
  console.log('second handler definition')
  / this will execute
}
t.addEventListener('complete', handlerOne)
t.addEventListener('complete', handlerTwo)
t.start()

Custom Events

Passing event data in this method involves using CustomEvent in place of the Event interface. Back in the Timer class, let's change the 'complete' event so that it exposes some event data.

The CustomEvent constructor takes an optional 'detail' argument. We can use the 'detail' object to hold any data we want available on the event.

Instead of declaring and initializing the event at construction, we'll create it at the time the event fires. This is useful if we want to pass time-sensitive data, such as a timestamp.

typescript
/ Timer.ts
export default class Timer extends EventTarget {
  constructor() {
    super()
  }

  public start(): void {
    setTimeout(() => {
      this.dispatchEvent(
        new CustomEvent('complete', { detail: { time: Date.now() } })
      )
    }, 7000)
  }
}

In the event handler, we now have access to the detail object on the event.

typescript
/ consumer.ts
import Timer from './Timer'

const t = new Timer()
const completeHandler = (e: CustomEvent) => {
  console.log('timer completed event', e.detail.time)
}
t.addEventListener('complete', completeHandler)
t.start()

Removing Event Handlers

You can remove an event handler with the removeEventListener method.

typescript
/ consumer.ts
t.removeEventListener('complete', completeHandler);

Event Listeners with EventEmitter

If you're working in a server-side context, such as with Node.js, you won't have access to the EventTarget class. Instead, Node.js has its own version, EventEmitter.

Working with the EventEmitter class is like working with EventTarget. Instead of extending EventTarget, our class will extend EventEmitter. Events fire with the emit method, which takes the event name as a string. You can pass any number of optional arguments as event data.

typescript
/ Timer.ts
import { EventEmitter } from 'events'
import { setTimeout } from 'timers'

export default class Timer extends EventEmitter {
  constructor() {
    super()
  }

  public start(): void {
    setTimeout(() => {
      this.emit('complete', { time: Date.now() })
    }, 7000)
  }
}

/ consumer.ts
import Timer from './Timer'

const t = new Timer()
t.on('complete', () => {
  console.log('timer completed event')
})
t.start()

Which Event Handler Type to Use

Each technique has its pros and cons. Deciding on which to use depends on the application requirements. Do you need a simple and lightweight approach? Use event property handlers. Do you need to register many handlers per event? Use the EventTarget or EventEmitter interface.

Was this article helpful to you?
 

Related ArticlesThese articles may also be of interest to you

CommentsShare your thoughts in the comments below

If you enjoyed reading this article, or it helped you in some way, all I ask in return is you leave a comment below or share this page with your friends. Thank you.

There are no comments yet. Why not get the discussion started?

We respect your privacy, and will not make your email public. Learn how your comment data is processed.