event, delegate.
Add the keyword "event", and make the event an explicit member of the class. Thus, you can check the event listeners, the correctness of the arguments when emitting events and etc.
class ClassWithEvents {
//declare a simple event without arguments
public event simpleEvent;
//declare a simple event with arguments
public event simpleEventWithArguments(arg: Arg1Type);
//declare a static event with reaction and arguments
public static event staticEventWithReaction(arg1: Arg1Type, arg2: Arg2Type) {
//The first argument of the listeners is always the sender of the event
add(listener: (sender: this, arg1: Arg1Type, arg2: Arg2Type) => void) {
//Do something when adding a listener
}
remove(listener: (sender: this, arg1: Arg1Type, arg2: Arg2Type) => void) {
//Do something when removing a listener
}
}
static emitEvent() {
//The emitter sends only arguments
//the "sender" parameter is sent by the system automatically.
this.staticEventWithReaction(new Arg1Type , new Arg2Type);
}
}
//Event interface
interface Event<SenderT, Arg1T, Arg2T/*etc*/> {
//Method adds/remove a listener
on(listener: (sender: SenderT, arg1: Arg1T, arg2: Arg2T) => void);
once(listener: (sender: SenderT, arg1: Arg1T, arg2: Arg2T) => void);
remove(listener: (sender: SenderT, arg1: Arg1T, arg2: Arg2T) => void);
//Method adds/remove a listener and binds context to it
on(context: any, listener: (sender: SenderT, arg1: Arg1T, arg2: Arg2T) => void);
once(context: any, listener: (sender: SenderT, arg1: Arg1T, arg2: Arg2T) => void);
//Method removes all event listeners
removeAll();
//Method emit event
emit(arg1: Arg1T, arg2: Arg2T);
//Method removes all listeners to all events
static removeAll(sender: SenderT);
}
//Use events
const myContext = {};
const listener = (sender: ClassWithEvents, arg1: Arg1Type, arg2: Arg2Type) => {
};
//add/remove listener
ClassWithEvents.staticEventWithReaction.on(listener);
ClassWithEvents.staticEventWithReaction.remove(listener);
//add/remove listener with manual context binding
const listenerWithContext = listener.bind(myContext);
ClassWithEvents.staticEventWithReaction.on(listenerWithContext);
ClassWithEvents.staticEventWithReaction.remove(listenerWithContext);
//add listener with auto context binding
ClassWithEvents.staticEventWithReaction.on(myContext, listener);
//removes all event listeners
ClassWithEvents.staticEventWithReaction.removeAll();
//removes all listeners to all events
Event.removeAll(ClassWithEvents);
function __eventAddListener(target, eventName, listener, context, once) {
if( typeof context !== "undefined" ) {
listener = listener.bind(context);
}
var events = target['__tsevents'];
if( !events ) {
events = target['__tsevents'] = {};
}
var event = events[eventName];
if( !event ) {
event = events[eventName] = [];
}
event.push({
listener: listener,
once: once || false
});
__eventAddListenerTrigger(target, eventName, listener);
}
function __eventRemoveListener(target, eventName?, listener?) {
var events = target['__tsevents'];
if( !events ) {
return;
}
else if( typeof eventName === "undefined" ) {
__eventRemoveListenerTrigger(target);
target['__tsevents'] = null;
return;
}
var event = events[eventName];
if( !event ) {
return;
}
else if( typeof listener === "undefined" ) {
__eventRemoveListenerTrigger(target, eventName);
events[eventName] = null;
return;
}
events[eventName] = event.filter(function(event) {
var isEquals = event.listener === listener;
__eventRemoveListenerTrigger(target, eventName, listener);
return !isEquals;
});
}
function __eventDeclareTrigger(target, eventName, trigger) {
var triggers = target['__tseventstriggers'];
if( !triggers ) {
triggers = target['__tseventstriggers'] = {};
}
triggers[eventName] = trigger;
}
function __eventRemoveListenerTrigger(target, eventName?, listener?) {
var triggers = target['__tseventstriggers'];
if( !triggers ) {
return;
}
else if( typeof eventName === "undefined" ) {
Object.keys(triggers).forEach(eventName => {
if( target['__tsevents'] && target['__tsevents'][eventName] ) {
target['__tsevents'][eventName].forEach(event => {
triggers[eventName].remove.call(target, event.listener)
});
}
});
return;
}
var trigger = triggers[eventName];
if( !triggers ) {
return;
}
else if( typeof listener === "undefined" ) {
if( target['__tsevents'] && target['__tsevents'][eventName] ) {
target['__tsevents'][eventName].forEach(event => {
trigger.remove.call(target, event.listener)
});
}
return;
}
if( target['__tsevents'] && target['__tsevents'][eventName] ) {
trigger.remove.call(target, listener);
}
}
function __eventAddListenerTrigger(target, eventName, listener) {
var triggers = target['__tseventstriggers'];
if( !triggers ) {
return;
}
var trigger = triggers[eventName];
if( !triggers ) {
return;
}
trigger.add.call(target, listener);
}
function __eventEmit(sender, eventName, ...args) {
var events = sender['__tsevents'];
if( !events ) {
return;
}
var event = events[eventName];
if( !event ) {
return;
}
event.forEach(event => {
event.listener(sender, ...args);
if( event.once ) {
__eventRemoveListener(sender, eventName, event.listener);
}
})
}
var ClassWithEvents = (function () {
function ClassWithEvents() {
}
ClassWithEvents.emitEvent = function () {
//The emiter sends only arguments
//the "sender" parameter is sent by the system automatically.
__eventEmit(this, 'staticEventWithReaction', new Arg1Type, new Arg2Type);
};
__eventDeclareTrigger(ClassWithEvents, 'staticEventWithReaction', {
add: function add() {
//Do something when adding a listener
},
remove: function remove() {
//Do something when removing a listener
}
})
return ClassWithEvents;
}());
//Use events
var myContext = {};
var listener = function (sender, arg1, arg2) {
};
//add/remove listener
__eventAddListener(ClassWithEvents, 'staticEventWithReaction', listener);
__eventRemoveListener(ClassWithEvents, 'staticEventWithReaction', listener);
//add/remove listener with manual context binding
var listenerWithContext = listener.bind(myContext);
__eventAddListener(ClassWithEvents, 'staticEventWithReaction', listenerWithContext);
__eventRemoveListener(ClassWithEvents, 'staticEventWithReaction', listenerWithContext);
//add listener with auto context binding
__eventAddListener(ClassWithEvents, 'staticEventWithReaction', listener, myContext);
//removes all event listeners
__eventRemoveListener(ClassWithEvents, 'staticEventWithReaction');
//removes all listeners to all events
__eventRemoveListener(ClassWithEvents);
My suggestion meets these guidelines:
I don't think this meets the guideline that:
This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
The event syntax is 'non-ECMAScript syntax' and it does have output in JavaScript. Such features are usually implemented only if they are part of a future ECMAScript standard (at stage 3) and have to just be poly-filled for lower target versions. If event is used in a future in the standard it might break the 'Typescript is a superset of JavaScript` goal.
That being said, there is a case to be made to include this in the language, but I think you might have to get it included in JS first :)
@dragomirtitian I think I'll be older by the time they include events in ECMAScript. And there they do not make much sense, just to explicitly define events. And in typescript it is very necessary. All the same, there is hope.
@geverges-oleg I don't want to be a downer here, but it seems to me a lot of your static typing concerns are solvable in TS today, just buy using an object to represent the event.
This (admittedly naive) implementation would fix the issues that are directly related to types.
type ThisParameterType<T extends Function> = T extends (this: infer U, ... a: any[]) => any ? U: object;
export class CustomEvent<TFn extends (...a: any[]) => void = () => void> {
constructor(private addRemove?: {
add: (listener: TFn) => void
remove: (listener: TFn) => void
}){
}
private listeners: TFn[] = [];
on<TActualFn extends TFn>(context: ThisParameterType<TActualFn>, fn: TActualFn) : void
on(fn: TFn): void
on(context: object | TFn, fn?: TFn): void{
if(typeof context !== "function") {
fn = fn!.bind(context) as TFn;
}else {
fn = context as TFn
}
if(this.addRemove) this.addRemove.add(fn!);
this.listeners.push(fn!);
}
emit(...args: Parameters<TFn>) {
this.listeners.forEach(fn => fn(...args));
}
remove(fn: TFn){
var index = this.listeners.indexOf(fn);
if(index > -1){
if(this.addRemove) this.addRemove.remove(this.listeners[index]);
this.listeners.splice(index, 1);
}
}
removeAll(){
this.listeners = []
}
static removeAll(target: any) {
Object.values(target)
.forEach(o => {
if(o instanceof CustomEvent) {
o.removeAll();
}
})
}
}
class Arg1Type { a!: number }
class Arg2Type { b!: number }
class ClassWithEvents {
//declare a simple event without arguments
public simpleEvent = new CustomEvent();
private _lazyEvent: CustomEvent | undefined;
public get lazyEvent () {
return this._lazyEvent || (this._lazyEvent = new CustomEvent());
}
//declare a simple event with arguments
public simpleEventWithArguments = new CustomEvent<(sender: this, arg: Arg1Type) => void>();
//declare a static event with reaction and arguments
public static staticEventWithReaction = new CustomEvent<(sender: ClassWithEvents, arg1: Arg1Type, arg2: Arg2Type) => void>({
//The first argument of the listeners is always the sender of the event
add(listener) {
//Do something when adding a listener
},
remove(listener) {
//Do something when removing a listener
}
});
static emitEvent(sender: ClassWithEvents) {
this.staticEventWithReaction.emit(sender, new Arg1Type , new Arg2Type);
}
}
//Use events
const myContext = {};
const listener = (sender: ClassWithEvents, arg1: Arg1Type, arg2: Arg2Type) => {
};
const badListener = (sender: ClassWithEvents, arg1: Arg2Type, arg2: Arg1Type) => {
}
//add/remove listener
ClassWithEvents.staticEventWithReaction.on(listener);
ClassWithEvents.staticEventWithReaction.on(badListener); // err
ClassWithEvents.staticEventWithReaction.remove(listener);
//add/remove listener with manual context binding
const listenerWithContext = listener.bind(myContext);
ClassWithEvents.staticEventWithReaction.on(listenerWithContext);
ClassWithEvents.staticEventWithReaction.remove(listenerWithContext);
//add listener with auto context binding
ClassWithEvents.staticEventWithReaction.on(myContext, listener);
//removes all event listeners
ClassWithEvents.staticEventWithReaction.removeAll();
ClassWithEvents.staticEventWithReaction.emit(new ClassWithEvents, new Arg1Type, new Arg2Type) // ok
ClassWithEvents.staticEventWithReaction.emit(new ClassWithEvents, new Arg1Type) // err emit args not ok
//removes all listeners to all events
CustomEvent.removeAll(ClassWithEvents);
I'm not sure I got the semantics of your static events 100% right, but regardless of implementation details which could be changed and optimized, this would fix most of your problmes
Added to this is the huge advantage of not having the language burdened with a non standard extension to JS that could potentially break in the future. Also gives flexibility of implementation to the developer.
@dragomirtitian covered all the relevant points here. This is the kind of feature we're actively avoiding implementing.
If you build events on decorators or getters, then a large number of objects are created that are not used
@dragomirtitian In this problem, I just meant what you offer. Lazy property doesn鈥檛 help much here, as many objects will be created during emitting of the event.
I agree, this approach helps to solve all other problems with type checking, but it creates a problem with performance and memory consumption. For high-load systems, this is unacceptable, so you have to use EventEmitter. If you make events at the compiler level, it will be perfect.
@dragomirtitian covered all the relevant points here. This is the kind of feature we're actively avoiding implementing.
@RyanCavanaugh Very sad.
You reject a lot of useful offers, either for reasons that this is not ECMAScript standards, or because it allegedly can spoil something. ECMAScript is developing very slowly, so typescript was like a breath of clean air and hope for convenient development tools. But lately, typescript has become more and more frustrating, with every release there are less and less really useful and necessary features.
Can you think about a separate compiler branch? Extended Typescript, which will include interesting and experimental features that are not in the ECMAScript standard, a huge number of developers will thank you for this.
TypeScript is Apache-licensed; no one will stop you.
Most helpful comment
@dragomirtitian I think I'll be older by the time they include events in ECMAScript. And there they do not make much sense, just to explicitly define events. And in typescript it is very necessary. All the same, there is hope.