Strongly typed events in TypeScript

Topics: General
Nov 7, 2012 at 9:52 AM
Edited Nov 7, 2012 at 10:07 AM

Creating strongly typed events for jQuery style bind() events is tough because of the way you have to pass a list of named events you want to bind to.  Since any number of events can be bound to in a single bind() call there's really no great way of strongly typing these events beyond just typing the actual params passed to bind().  Furthermore, there's an even bigger issue with bind() style events in that there's no way of discovering (via tooling) what events a class actually supports.  Basically, bind() style events have major issues when it comes to a strongly typed language like TypeScript.

I thought I'd share with the community a pattern that I've been using for my TypeScript classes which allows for exposing a strongly typed set of events from a class:

// Base IEvent interface and implementation

interface IEvent {
    add(listener: () => void): void;
    remove(listener: () => void ): void;
    trigger(...a:any[]): void;
}

class TypedEvent implements IEvent {
    // Private member vars
	private _listeners: any[] = [];

	public add (listener: () => void): void {
		/// <summary>Registers a new listener for the event.</summary>
		/// <param name="listener">The callback function to register.</param>
		this._listeners.push(listener);
	}
	public remove (listener?: () => void): void {
		/// <summary>Unregisters a listener from the event.</summary>
		/// <param name="listener">The callback function that was registered. If missing then all listeners will be removed.</param>
        if (typeof listener === 'function') {
		    for (var i = 0, l = this._listeners.length; i < l; l++) {
			    if (this._listeners[i] === listener) {
				    this._listeners.splice(i, 1);
				    break;
			    }	
		    }
        } else {
            this._listeners = [];
        }
    }

    public trigger (...a: any[]): void {
		/// <summary>Invokes all of the listeners for this event.</summary>
		/// <param name="args">Optional set of arguments to pass to listners.</param>
		var context = {};
		var listeners = this._listeners.slice(0);
		for(var i = 0, l = listeners.length; i < l; i++) {
		    listeners[i].apply(context, a || []);
		}
	}
}
	
// Exposing events
interface IMessageEvent extends IEvent {
    add(listener: (message: string) => void): void;
    remove(listener: (message: string) => void ): void;
    trigger(message: string): void;
}

class Foo {
	// Events
	public onMessage: IMessageEvent = new TypedEvent();
	
	// Methods
	public bar(): void {
		this.onMessage.trigger('event fired');
	}
}

// Consuming events
var foo = new Foo();
foo.onMessage.add((message) => {
	alert(message);
});
foo.bar();

If you run the code snippet above in the playground you should see an alert() pop saying 'event fired'.  This pattern uses interfaces and takes advantage of TypeScripts ability to extend an inherited methods signature.  

To declare new events you simply define a new interface that extends IEvent and strongly types the arguments for add(), remove(), and trigger().  Your class can then expose an instance of this event as a property that casts a new instance of TypedEvent() to your interface. Consumers can then listen for events by calling add in pretty much the same way you would have called bind() before.  Now you just have a separate property for each event supported by the class so no more discover issue.

At compile time all of these interfaces boil out so there's really no significant overhead added over the top of how bind() events work and in general should be just as performant.  The tooling improvement, however, is a massive upgrade over bind() style events.

Dec 7, 2012 at 2:30 PM

Wanted to thank you for posting this. Exactly what I was looking for. Hopefully in the future, TypeScript will support more native events like C# does.

Dec 7, 2012 at 5:20 PM

If you are looking for DOM-less events, you might look into a simple little library that I wrote a month or two ago. You can find it on GitHub as EventDispatcher-ts. It was inspired by my work with ActionScript 3. Personally, I've always liked the descriptiveness of naming such as "addEventListener" or "dispatchEvent." Anyhow, feel free to fork, modify, and send pull requests. I'm always open to improvements.

Dec 8, 2012 at 2:00 AM

Thanks jdmichal, we wanted simple C# style events that could be strongly typed and this is the simplest way of achieving that we've found. 

jmvrbanaca, I looked at EventDispatcher-ts and I think if you're looking for something that supports "named" events it's a pretty good option.  One suggestion I'd make though is that instead of making _listeners an array you have to loop over you should use an object that's indexed by type as your operations will be more efficient.  I hope you don't mind but I made a pass at updating your code to reflect this. I also made things a little more strongly typed:

 

export class Event {
	private _type:string;
	private _target;

	constructor(type:string, targetObj:any) {
		this._type = type;
		this._target = targetObj;
	}

	public getTarget():any {
		return this._target;
	}

	public getType():string {
		return this._type;
	}
}
interface EventListener {
	(evt: Event): void;
}

export class EventDispatcher {
	private _events: { [x:string]: EventListener[]; } = {};

	public hasEventListener(type:string, listener: EventListener): bool {
		var listeners = this._events[type];
		if (listeners) {
			for (var i = 0, l = listeners.length; i < l; i++) {
				if (listeners[i] === listener) {
					return true;
				}
			}
		}
		return false;
	}

	public addEventListener (type: string, listener: EventListener): void {
		// Not sure you absolutely need this test
		if (!this.hasEventListener(type, listener)) {
			var listeners = this._events[type];
			if (!listeners) {
				listeners = this._events[type] = [];
			}
			listeners.push(listener);
		}
	}

	public removeEventListener (type: string, listener: EventListener): void {
		var listeners = this._events[type];
		if (listeners) {
			for (var i = 0, l = listeners.length; i < l; i++) {
				if (listeners[i] === listener) {
					listeners.splice(i, 1);
					break;
				}
			}
		}
	}

	public dispatchEvent (evt: Event, thisArg = {}): void {
		var listeners = this._events[evt.getType()];
		if (listeners) {
			for (var i = 0, l = listeners.length; i < l; i++) {
				listeners[i].call(thisArg, evt);
			}
		}
	}
}

   

Dec 8, 2012 at 9:43 PM

Woops! I misunderstood. You actually want types to denote events like in C#. Got it... Thanks for the input though. I'll look into making the modifications when I go on vacation in a week or so. 

As a side note, does anyone know the performance ramifications of using object type lookup vs a primitive string lookup in JavaScript? I know that in JavaScript you can have serious performance reductions when you start using the object variants of primitives, but I have never tried to test the performance of object type lookups in JS. 

Dec 10, 2012 at 12:06 PM
Edited Dec 12, 2012 at 10:29 AM

I went for a reverse map enum/proxy jquery approach

edit: added $.proxy for this

module com.company.app.events {
    
    export enum EventType {
        ApplicationReady
    }
    
    export class Event {
        private _eventType: EventType;
        Type(): EventType { return this._eventType; }

        private _caller: any;
        Caller(): any { return this._caller; }
        SetCaller(caller: any) { this._caller = caller; }

        private _userInfo: any;
        UserInfo(): any { return this._userInfo; }
        SetUserInfo(info: any) { this._userInfo = info; }

        constructor (eventType: EventType, userInfo?: any) {
            this._eventType = eventType;
            this._userInfo = userInfo;
        }
    }    

    export class EventController {
        constructor() {                    
            // Provide a global accessor a la singleton
            window['company'] = window['company'] || {};
            window['company']['eventcontroller'] = self;
        }
        
        Subscribe(event:EventType, fn:any, proxy: any) { 
	    var eventName:string = EventType['_map'][event];
	    $(this).bind(eventName, jQuery.proxy(fn, proxy));
	}
 
        Post(eventType:EventType, caller?:any, userInfo?:any) {
            var eventName:string = EventType['_map'][eventType];
            var event:Event = new Event(eventType);
            event.SetCaller(caller);
            event.SetUserInfo(userInfo);
            $(this).triggerHandler(eventName, [event]);
        }

        static DefaultEventController(): EventController {
            return <EventController>window['company']['eventcontroller'];
        }
    }
}

// Create
var eventController = new Events.EventController();

// Subscribe
eventController.Subscribe(Events.EventType.ApplicationReady, function(evt, event:Event) {
    console.log('Application ready! Caller is ' + event.Caller() + ' userInfo is ' +  event.UserInfo());
}, this);

// Post
eventController.Post(Events.EventType.ApplicationReady, this);   

Dec 14, 2012 at 2:40 PM

I ended up using ickman's code thanks!

I added this event to pass data out.

interface IDataChangeEvent extends IEvent {
    add(listener: (data: any) => void): void;
    remove(listener: (data: any) => void ): void;
    trigger(data: any): void;
}

It's working well!  Nice and simple.

Dec 14, 2012 at 8:41 PM

Thanks Jon... This systems been working pretty well for us too. 

 

-steve

Dec 14, 2012 at 8:47 PM
Edited Dec 14, 2012 at 8:48 PM

I should add a note for others that might use this approach.  You'll notice there's a remove() method for unbinding events and that method works but there's a subtle caveat.  If you plan to remove an added listener make sure the thing you added is a 'var' and what you remove is the SAME 'var' otherwise it won't find your handler.  This is because I use "===" to compare listeners. Below is a revised example of that:

 

// Consuming events
var foo = new Foo();
var handler = () => {
	alert(message);
};
foo.onMessage.add(handler);
foo.bar();
foo.onMessage.remove(handler);

Jun 13, 2013 at 10:20 PM
I am trying to use the suggested method of creating and consuming events; but I am running into an issue - can someone please take a look at my code and let me know what I am doing incorrectly? The error I get is when the button is clicked and the Dialog class tried to call the trigger on the onSubmit event (Error = Uncaught TypeError: Cannot call method 'trigger' of undefined). I cannot figure out why onSubmit is undefined.

myclasses.ts
// Base IEvent Interface
interface IEvent {...}

class TypedEvent implements IEvent {...}

// Exposing events
interface IMessageEvent extends IEvent {...}

// Module
module myClasses {
// Class
export class Dialog {
...
    // EVENTS - TEST PROCESSING
    onSubmit: IMessageEvent = new TypedEvent();

    // Constructor
    constructor() {
...
        // Get a reference to the buttons
        this.btnSubmit = $('#signInBtnSubmit');

        // CREATE EVENT HANDLERS
        this.btnSubmit.click(function () {
            this.onSubmit.trigger('event fired');
        });
     ...
    }
...
}
}

app.ts
...
    var newDialog = new MyClasses.Dialog();
    var handler = (message) => {
        alert('Pressed Dialog Submit' + message);
    };
    newDialog.onSubmit.add(handler);
...
Jun 13, 2013 at 10:30 PM
Edited Jun 13, 2013 at 10:41 PM
Well I thought I had it figured out; help still needed.

"I think I figured it out; it was a variable scoping error; I had to define my function that raises the event as public (as indicated) and then call that from my button click event virtual method. (Duh!)."

Thanks,
Jun 13, 2013 at 11:16 PM
this.btnSubmit.click(function () {
      this.onSubmit.trigger('event fired');
});
In the above code, your second "this" is not the "this" you think it is.. :)
Try using the Fat Arrow syntax instead, as that preserve the class "this"
this.btnSubmit.click( () => {
      this.onSubmit.trigger('event fired');
});
Oct 13, 2013 at 5:36 PM
With the help of generics, there can be defined generic interfaces for ickman's code:

events.ts
export interface I1ArgsEvent<T> extends IEvent {
    add(listener: (message: T) => any): void;
    remove(listener: (message: T) => any): void;
    trigger(message: T): void;
}

export interface I2ArgsEvent<T, U> extends IEvent {
    add(listener: (message1: T, message2: U) => any): void;
    remove(listener: (message: T, message2: U) => any): void;
    trigger(message: T, message2: U): void;
}
Events can then be written with the concrete parameter types as arguments
import Events = require("events.ts");

class Foo {
    // Events
    public onMessage: Events.I1ArgsEvent<string>= new Events.TypedEvent();
        public onTwoArgsMessage: Events.I2ArgsEvent<string, number> = new Events.TypedEvent();
    
    // Methods
    public bar(): void {
        this.onMessage.trigger('event fired');
                this.onTwoArgsMessage.trigger('event fired', 2);
    }
}
Nov 7, 2013 at 2:57 PM
Edited Nov 7, 2013 at 3:40 PM
I found the answer to my own question.
Nov 8, 2013 at 9:29 AM
I personally use this :
'use strict';

export interface ISignal<T> {
    add(listener: (parameter: T) => any, priority?: number): void;
    remove(listener: (parameter: T) => any): void;
    dispatch(parameter: T): boolean;
    clear(): void;
    hasListeners(): boolean;
}

export class Signal<T> implements ISignal<T> {
    private listeners: { (parameter: T): any }[] = [];
    private priorities: number[] = [];
    
    add(listener: (parameter: T) => any, priority = 0): void {
        var index = this.listeners.indexOf(listener);
        if (index !== -1) {
            this.priorities[index] = priority;
            return;
        }
        for (var i = 0, l = this.priorities.length; i < l; i++) {
            if (this.priorities[i] < priority) {
                this.priorities.splice(i, 0, priority);
                this.listeners.splice(i, 0, listener);
                return;
            }
        }
        this.priorities.push(priority);
        this.listeners.push(listener);
    }
    
    remove(listener: (parameter: T) => any): void {
        var index = this.listeners.indexOf(listener);
        if (index >= 0) {
            this.priorities.splice(index, 1);
            this.listeners.splice(index, 1);
        }
    }
    
    dispatch(parameter: T): boolean {
        var indexesToRemove: number[];
        var hasBeenCanceled = this.listeners.every((listener: (parameter: T) => any) =>  {
            var result = listener(parameter);
            return result !== false;
        });
        
        return hasBeenCanceled;
    }
    
    clear(): void {
        this.listeners = [];
        this.priorities = [];
    }
    
    hasListeners(): boolean {
        return this.listeners.length > 0;
    }
}


export class JQuerySignalWrapper<JQueryEventObject> implements ISignal<JQueryEventObject>  {
  
    constructor(
        private target: JQuery, 
        private event: string
    ) {}
    
    private signal: Signal<JQueryEventObject> = new Signal<JQueryEventObject>();
    private jqueryEventHandler = (parameter: JQueryEventObject) => {
        this.signal.dispatch(parameter);
    }    
    
    add(listener: (parameter: JQueryEventObject) => any, priority?: number): void {
        this.signal.add(listener, priority);
        this.target.on(this.event, this.jqueryEventHandler); 
    }
    
    remove(listener: (parameter: JQueryEventObject) => any): void {
        this.signal.remove(listener);
        if (!this.hasListeners()) {
            this.removeJQueryEventListener();
        }
    } 
    
    dispatch(parameter: JQueryEventObject): boolean {
        return this.signal.dispatch(parameter);
    }
    
    clear(): void {
        this.signal.clear();
        this.removeJQueryEventListener();
    }
    
    hasListeners(): boolean {
        return this.signal.hasListeners();
    }
    
    private removeJQueryEventListener() {
        this.target.off(this.event, this.jqueryEventHandler);
    }
}


export class DomSignalWrapper<T extends Event> implements ISignal<T>  {
  
    constructor(
        private target: EventTarget, 
        private event: string,
        private capture: boolean
    ) {}
    
    private signal: Signal<T> = new Signal<T>();
    private eventHandler = (parameter: T) => {
        this.signal.dispatch(parameter);
    }    
    
    add(listener: (parameter: T) => any, priority?: number): void {
        this.signal.add(listener, priority);
        this.target.addEventListener(this.event, this.eventHandler, this.capture);
    }
    
    remove(listener: (parameter: T) => any): void {
        this.signal.remove(listener);
        if (!this.hasListeners()) {
            this.removeEventListener();
        }
    } 
    
    dispatch(parameter: T): boolean {
        return this.signal.dispatch(parameter);
    }
    
    clear(): void {
        this.signal.clear();
        this.removeEventListener();
    }
    
    hasListeners(): boolean {
        return this.signal.hasListeners();
    }
    
    private removeEventListener() {
        this.target.removeEventListener(this.event, this.eventHandler, this.capture);
    }
}
Mar 4, 2014 at 10:52 AM
My implementation:
class EventArgs {
    public static get Empty(): EventArgs {
        return new EventArgs();
    }
}

interface IEventHandler<T extends EventArgs> {
    handle(sender: any, e: T): void;
}

class EventHandler<T extends EventArgs> implements IEventHandler<T> {
    private _handler: { (sender: any, e: T): void };

    constructor(handler: { (sender: any, e: T): void }) {
        this._handler = handler;
    }

    public handle(sender: any, e: T): void {
        this._handler(sender, e);
    }
}

interface IDelegate<T extends EventArgs> {
    subscribe(eventHandler: IEventHandler<T>): void;
    unsubscribe(eventHandler: IEventHandler<T>): void;
}

class Delegate<T extends EventArgs> implements IDelegate<T> {
    private _eventHandlers: Array<IEventHandler<T>>;

    constructor() {
        this._eventHandlers = new Array<IEventHandler<T>>();
    }

    public subscribe(eventHandler: IEventHandler<T>): void {
        if (this._eventHandlers.indexOf(eventHandler) == -1) {
            this._eventHandlers.push(eventHandler);
        }
    }

    public unsubscribe(eventHandler: IEventHandler<T>): void {
        var i = this._eventHandlers.indexOf(eventHandler);
        if (i != -1) {
            this._eventHandlers.splice(i, 1);
        }
    }

    public raise(sender: any, e: T): void {
        for (var i = 0; i < this._eventHandlers.length; i++) {
            this._eventHandlers[i].handle(sender, e);
        }
    }
}
How to use it:
class A {
    private _changeOccurred: Delegate<EventArgs>;

    public get ChangeOccurred(): IDelegate<EventArgs> {
        return this._changeOccurred;
    }

    constructor() {
        this._changeOccurred = new Delegate<EventArgs>();
    }

    public makeChange() {
        this._changeOccurred.raise(this, EventArgs.Empty);
    }
}

class B {
    private _changeOccurredEventHandler: IEventHandler<EventArgs>;

    constructor(a: A) {

        this._changeOccurredEventHandler = new EventHandler<EventArgs>((sender: any, e: EventArgs) => {
            this.onChangeOccurred(sender, a);
        });
        a.ChangeOccurred.subscribe(this._changeOccurredEventHandler);
    }

    private onChangeOccurred(sender: any, e: EventArgs) {

    }
}