78

I am just wondering if in TypeScript you can define custom events on your classes or interfaces?

What would this look like?

Fenton
  • 241,084
  • 71
  • 387
  • 401
CompareTheMooCat
  • 1,047
  • 2
  • 10
  • 14
  • Check this out:- http://stackoverflow.com/questions/12756423/is-there-an-alias-for-this-in-typescript – Rahul Tripathi Oct 14 '12 at 10:10
  • 1
    A few implementations out there for events today. Here's [sub-events](https://github.com/vitaly-t/sub-events), as one of them. – vitaly-t Sep 23 '19 at 21:42

10 Answers10

131

How about this simplified event to be used as a property? Stronger typing of the owning class and no inheritance requirement:

interface ILiteEvent<T> {
    on(handler: { (data?: T): void }) : void;
    off(handler: { (data?: T): void }) : void;
}

class LiteEvent<T> implements ILiteEvent<T> {
    private handlers: { (data?: T): void; }[] = [];

    public on(handler: { (data?: T): void }) : void {
        this.handlers.push(handler);
    }

    public off(handler: { (data?: T): void }) : void {
        this.handlers = this.handlers.filter(h => h !== handler);
    }

    public trigger(data?: T) {
        this.handlers.slice(0).forEach(h => h(data));
    }

    public expose() : ILiteEvent<T> {
        return this;
    }
}

used like so:

class Security {
    private readonly onLogin = new LiteEvent<string>();
    private readonly onLogout = new LiteEvent<void>();

    public get LoggedIn() { return this.onLogin.expose(); } 
    public get LoggedOut() { return this.onLogout.expose(); }

    // ... onLogin.trigger('bob');
}

function Init() {
    var security = new Security();

    var loggedOut = () => { /* ... */ }

    security.LoggedIn.on((username?) => { /* ... */ });
    security.LoggedOut.on(loggedOut);

    // ...

    security.LoggedOut.off(loggedOut);
}

Improvements?

A gist for this

boop
  • 7,413
  • 13
  • 50
  • 94
Jason Kleban
  • 20,024
  • 18
  • 75
  • 125
  • if two different instances attach same method, i think "off" can then remove incorrect one. (since methods are on prototype so they are same instance for each object instance) – Kikaimaru Mar 19 '13 at 19:26
  • @Kikaimaru - I don't understand. Can you elaborate? – Jason Kleban Mar 20 '13 at 11:13
  • If you want to remove a handler, you should probably keep a reference to the handler function, like so: `var handler1 = function(data) { alert('handler1'); }; var handler2 = function(data) { alert('handler2'); }; var ev = new Event(); ev.on(handler1); ev.on(handler2); ev.trigger(); ev.off(handler1); ev.handlers; alert('Invoke round 2'); ev.trigger();` – VeeTheSecond May 26 '13 at 15:22
  • 2
    Nice solution. Don't forget to have `Event` implement `IEvent`. – VeeTheSecond May 26 '13 at 15:25
  • Why don't you use the addEventListener/removeEventListener convention ? It would make your example easier to understand. – bokan Jun 09 '14 at 10:18
  • I'm just following the jQuery event handler convention instead which I like because the names are shorter. And `addEventListener/removeEventListener` are browser-specific anyway. – Jason Kleban Jun 09 '14 at 12:25
  • 4
    suggest doing this.handlers.slice(0).forEach(h => h(data)); instead of this.handlers.forEach(h => h(data)); – morpheus Jul 21 '14 at 03:14
  • @morpheus - I agree - because one of the handlers could modify .handlers (indirectly) and make inconsistent behavior which depends on the hidden internal ordering of the handler execution. – Jason Kleban Jul 21 '14 at 14:37
  • 2
    Why the check on `if (this.handlers)` in the `trigger` method? Isn't it always true? – Tarion Oct 21 '15 at 14:40
  • 2
    @Tarion - yes, looks to me like that check can be omitted. I assume I made some conceptual carry-over from the way event handlers need to be null-checked before invocation in .NET but looks to me like the handlers member will always be non-null and that it will be an array. If it weren't an array the code would probably fail anyway. – Jason Kleban Oct 21 '15 at 14:49
  • `noob alert` - This is awesome, after going through the internet in loops I eventually understood this script. But another problem was I was executing code in the constructor and raising event, and only afterwards binding to an event. Such a noob I am! but this is truly beautiful, lite solution! +6pack! – Piotr Kula Dec 18 '15 at 17:50
  • looks like a great solution. Any ideas why I'm receiving this? `error TS7010: 'on', which lacks return-type annotation, implicitly has an 'any' return type.` with regards to `on(handler: { (data?: T): void });`? – Kevin Mar 14 '16 at 21:09
  • Not totally following the implementation here but is this the same thing? `on(handler: { (data?: T): void }):void;`. It does not throwing a compilation error. – Kevin Mar 14 '16 at 21:23
  • why not just make the field public instead of using a getter? – Ilia Choly Apr 07 '16 at 03:37
  • @Kevin - yes, `no implicit any` default option was new since the original. `any` type would suffice, but `void` is more informative, and appropriately, more restrictive. – Jason Kleban May 19 '16 at 14:22
  • @iliacholy - used a getter instead of a public field 1) because `ILiteEvent` only exposed `on()` and `off()` while the `LiteEvent` implementation also has `trigger()` which we normally wouldn't want to expose directly and 2) to avoid the `LiteEvent` from accidentally being overwritten. In the current version, TypeScript doesn't actually stop you from setting it, [but it will](https://github.com/Microsoft/TypeScript/issues/12). – Jason Kleban May 19 '16 at 14:56
  • I updated the answer and the gist to use `readonly` and a new `.expose()` to avoid having to explicitly state the `ILiteEvent` type and type parameter of the exposed event member. – Jason Kleban Feb 22 '17 at 20:18
  • This feel weird, instead of `security.LoggedIn.on((username?) => { ... });` it should follow JS event structure: `security.on(LoggedIn, (username?) => { ... });` – Ivan Castellanos Feb 03 '19 at 01:21
  • I think data should respect T exactly and not changing it to T? (data?). If it is optional, it should be specified in T. Except for that, it is great :) – Yepeekai Apr 11 '19 at 15:22
  • @Yepeekai - well, you can't specify optionality on the type iteself. Optionality is a feature of a parameter (or member) itself. This optionality is to support `void` events that have no data and therefore require no argument. I've just made [this stricter version](https://gist.github.com/JasonKleban/924babb9c56d697c2d2a8f6f604eb3d4) for you using conditional types (not tested). I wonder if it makes an type inferences/error messages harder to read? – Jason Kleban Apr 11 '19 at 16:39
  • 1
    `number?` == `number | undefined` So I can call `new LiteEvent()` Also for `void`, I can call `new LiteEvent()` and then `ev.trigger();` will work – Yepeekai Apr 11 '19 at 20:56
  • 1
    I did not realize that void parameters can be omitted. Seems related to the conversation in this [still-open-but-closed-as-a-dupe issue](https://github.com/Microsoft/TypeScript/issues/19259)? – Jason Kleban Apr 12 '19 at 10:52
  • is calling `off` necessary for every event handler? or should I omit it without worrying about memory leaks? – michal.jakubeczy May 22 '20 at 10:22
  • This solution is nice and light, but does have the disadvantage that the subscriber must be aware of the publisher. In a more complete event bus implementation, publishers would be totally decoupled from subscribers and visa versa. That extra layer of indirection adds a lot of flexibility. But as a lighter solution, this is very useful. – Ezward Nov 13 '20 at 01:42
  • Realise this is an old solution, but would the better way to do this in 2020 not be to simply extend the EventEmitter class available out of the box in node (or as a 3rd party dep - https://www.npmjs.com/package/events - in the browser) and be done with it? This is the standard for events these days and comes with TypeScript support. – Will Taylor Dec 10 '20 at 11:55
  • @ezward that is not necessarily a disadvantage, depends on the use case. Observer and Pub-Sub are 2 separate patterns (this example being Observer): https://hackernoon.com/observer-vs-pub-sub-pattern-50d3b27f838c – Will Taylor Dec 10 '20 at 11:59
  • This is just _The Observer Pattern_, almost verbatim. – Cody Nov 03 '21 at 22:46
23

The NPM package Strongly Typed Events for TypeScript (GitHub) implements 3 types of events: IEvent<TSender, TArgs>, ISimpleEvent<TArgs> and ISignal. This makes it easier to use the right kind of event for your project. It also hides the dispatch method from your event, as good information hiding should do.

Event Types / Interfaces - The definitions of the events:

interface IEventHandler<TSender, TArgs> {
    (sender: TSender, args: TArgs): void
}

interface ISimpleEventHandler<TArgs> {
    (args: TArgs): void
}

interface ISignalHandler {
    (): void;
}

Example - This example shows how the 3 types of events can be implemented using a ticking clock:

class Clock {

    //implement events as private dispatchers:
    private _onTick = new SignalDispatcher();
    private _onSequenceTick = new SimpleEventDispatcher<number>();
    private _onClockTick = new EventDispatcher<Clock, number>();

    private _ticks: number = 0;

    constructor(public name: string, timeout: number) {
        window.setInterval( () => { 
            this.Tick(); 
        }, timeout);
    }

    private Tick(): void {
        this._ticks += 1;

        //trigger event by calling the dispatch method and provide data
        this._onTick.dispatch();
        this._onSequenceTick.dispatch(this._ticks);
        this._onClockTick.dispatch(this, this._ticks);
    }

    //expose the events through the interfaces - use the asEvent
    //method to prevent exposure of the dispatch method:
    public get onTick(): ISignal {
        return this._onTick.asEvent();
    }

    public get onSequenceTick() : ISimpleEvent<number>{
        return this._onSequenceTick.asEvent();
    }

    public get onClockTick(): IEvent<Clock, number> {
        return this._onClockTick.asEvent();
    }
}

Usage - It can be used like this:

let clock = new Clock('Smu', 1000);

//log the ticks to the console
clock.onTick.subscribe(()=> console.log('Tick!'));

//log the sequence parameter to the console
clock.onSequenceTick.subscribe((s) => console.log(`Sequence: ${s}`));

//log the name of the clock and the tick argument to the console
clock.onClockTick.subscribe((c, n) => console.log(`${c.name} ticked ${n} times.`))

Read more here: On events, dispatchers and lists (a general explanation of the system)

Tutorials
I've written a few tutorials on the subject:

Kees C. Bakker
  • 32,294
  • 27
  • 115
  • 203
11

I think you are asking if a class instance can implement addEventListener() and dispatchEvent() like a DOM element. If the class is not a DOM node, then you would have to write your own event bus. You would define an interface for a class that can publish events, then implement the interface in the your classes. Here is a naive example;

interface IEventDispatcher{
  // maintain a list of listeners
  addEventListener(theEvent:string, theHandler:any);

  // remove a listener
  removeEventListener(theEvent:string, theHandler:any);

  // remove all listeners
  removeAllListeners(theEvent:string);

  // dispatch event to all listeners
  dispatchAll(theEvent:string);

  // send event to a handler
  dispatchEvent(theEvent:string, theHandler:any);
}

class EventDispatcher implement IEventDispatcher {
  private _eventHandlers = {};

  // maintain a list of listeners
  public addEventListener(theEvent:string, theHandler:any) {
    this._eventHandlers[theEvent] = this._eventHandlers[theEvent] || [];
    this._eventHandlers[theEvent].push(theHandler);
  }

  // remove a listener
  removeEventListener(theEvent:string, theHandler:any) {
    // TODO
  }

  // remove all listeners
  removeAllListeners(theEvent:string) {
    // TODO
  }

  // dispatch event to all listeners
  dispatchAll(theEvent:string) {
    var theHandlers = this._eventHandlers[theEvent];
    if(theHandlers) {
      for(var i = 0; i < theHandlers.length; i += 1) {
        dispatchEvent(theEvent, theHandlers[i]);
      }
    }
  }

  // send event to a handler
  dispatchEvent(theEvent:string, theHandler:any) {
    theHandler(theEvent);
  }
}
Ezward
  • 17,327
  • 6
  • 24
  • 32
2

You can use custom events in TypeScript. I'm not sure exactly what you are trying to do, but here is an example:

module Example {
    export class ClassWithEvents {
        public div: HTMLElement;

        constructor (id: string) {
            this.div = document.getElementById(id);

            // Create the event
            var evt = document.createEvent('Event');
            evt.initEvent('customevent', true, true);

            // Create a listener for the event
            var listener = function (e: Event) {
                var element = <HTMLElement> e.target;
                element.innerHTML = 'hello';
            }

            // Attach the listener to the event
            this.div.addEventListener('customevent', listener);

            // Trigger the event
            this.div.dispatchEvent(evt);
        }
    }
}

If you are looking to do something more specific please let me know.

Fenton
  • 241,084
  • 71
  • 387
  • 401
2

You can use rxjs to achieve this.

Declare following in your class:

export class MyClass {
    private _eventSubject = new Subject();
   
    public events = this._eventSubject.asObservable();

    public dispatchEvent(data: any) {
        this._eventSubject.next(data);
    }
}

And then you can trigger the event this way:

let myClassInstance = new MyClass();
myClassInstance.dispatchEvent(data);

And listen to this event in a following way:

myClassInstance.events.subscribe((data: any) => { yourCallback(); });
michal.jakubeczy
  • 8,221
  • 1
  • 59
  • 63
1

If you are looking to get intelli-sense type checking using the standard emitter pattern you can now do the following:

type DataEventType = "data";
type ErrorEventType = "error";
declare interface IDataStore<TResponse> extends Emitter {
    on(name: DataEventType, handler : (data: TResponse) => void);   
    on(name: ErrorEventType, handler: (error: any) => void);    
}
0

This solution allows you to directly write the parameters in the function call instead of needing to wrap all your parameters in an object.

interface ISubscription {
   (...args: any[]): void;
}

class PubSub<T extends ISubscription> {
    protected _subscribed : ISubscriptionItem[] = [];

    protected findSubscription(event : T) : ISubscriptionItem {
        this._subscribed.forEach( (item : ISubscriptionItem) =>{
            if (item.func==event)
              return item;
        } );
        return null;
    }

    public sub(applyObject : any,event : T) {
        var newItem = this.findSubscription(event);
        if (!newItem) {
            newItem = {object : applyObject, func : event };
            this._subscribed.push(newItem);
            this.doChangedEvent();
        }
    }
    public unsub(event : T) {
        for ( var i=this._subscribed.length-1 ; i>=0; i--) {
            if (this._subscribed[i].func==event)
                this._subscribed.splice(i,1);
        }
        this.doChangedEvent();
    }
    protected doPub(...args: any[]) {
        this._subscribed.forEach((item : ISubscriptionItem)=> {
            item.func.apply(item.object, args);
        })
    }

    public get pub() : T {
        var pubsub=this;
        var func=  (...args: any[]) => {
            pubsub.doPub(args);
        }
        return <T>func;
    }

    public get pubAsync() : T {
        var pubsub=this;
        var func =  (...args: any[]) => {
            setTimeout( () => {
                pubsub.doPub(args);
            });
        }
        return <T>func;
    }

    public get count() : number {
        return this._subscribed.length
    }

}

Usage:

interface ITestEvent {
    (test : string): void;
}

var onTestEvent = new PubSub<ITestEvent>();
//subscribe to the event
onTestEvent.sub(monitor,(test : string) => {alert("called:"+test)});
//call the event
onTestEvent.pub("test1");
0

Here's a simple example of adding custom-type events to your class, using sub-events:

class MyClass {

    readonly onMessage: SubEvent<string> = new SubEvent();
    readonly onData: SubEvent<MyCustomType> = new SubEvent();

    sendMessage(msg: string) {
        this.onMessage.emit(msg);
    }

    sendData(data: MyCustomType) {
        this.onData.emit(data);
    }
}

And then any client can subscribe to receive those events:

const a = new MyClass();

const sub1 = a.onMessage.subscribe(msg => {
    // msg here is strongly-typed
});

const sub2 = a.onData.subscribe(data => {
    // data here is strongly-typed
});

And when you no longer need the events, you can cancel the subscriptions:

sub1.cancel();

sub2.cancel();
vitaly-t
  • 24,279
  • 15
  • 116
  • 138
0

It's possible to use string as type directly, so:

export declare class Foo extends EventEmitter {
  addListener(event: 'connect', listener: () => void): this;
  addListener(event: 'close', listener: (hadError: boolean) => void): this;
}

For more example, check @types/node implementation

You may need VSCode 'convert overload list to single signature' refactor or // eslint-disable-next-line no-dupe-class-members to avoid ESLint complaints.

clarkttfu
  • 577
  • 6
  • 11
-1

You can find an event dispatcher declaration at YouTube. Following the video you will be able to have a fully typed version of the event dispatcher

Jailen
  • 17
  • 2