Reputation: 626
In TypeScript, I have a class that extends the module Events
which has the following in its declaration:
on(event: string | symbol, listener: (...args: any[]) => void): this;
The class that extends the module emits a number of events that have different signatures for the listener. I can create multiple overrides for this property that a slightly more specific, but still match the signature. Something like:
export class Agent extends Events {
constructor(conf: IAgentConf);
on(event: 'eventA', listener: (body: IAEvent) => void): this
on(event: 'eventB', listener: (body: IPayload<IBEvent>) => void):this;
on(event: 'eventC', listener: (body: ICEvent[]) => void): this;
...
}
Using this typings, TypeScript can identify the shape of the callback when declaring the event listeners.
However, I'm running into problems when I go to extend this object further, with a new object that emits a new event:
class MyAgent extends Agent {
static EventD: string = 'EventD';
init: () => void;
on(event: 'EventD', listener: (body: IEventD) => void):this;
constructor(conf: IAgentConf) {
super(conf);
this.init = () => {
this.on('EventA', body => {
this.emit(MyAgent.EventD, body.thingy);
});
};
init();
}
}
This unfortunately doesn't work. I get the error:
(TS) Property 'on' in type 'MyAgent' is not assignable to the same property in base type 'Agent'.
Is it possible to further override a property from a grandparent class in the grandchild class?
Upvotes: 0
Views: 832
Reputation: 30919
In general, a subclass can override a property (or method) of the superclass as long as the subclass's property is assignable to the parent class's property. For a method with multiple call signatures, I believe the rule is that each call signature in the superclass must have a corresponding call signature in the subclass that is assignable to it. This "assignable" relation is a little muddy because the parameters are compared bivariantly.
For the methods to be compatible in your case, every class needs to redeclare all the event types from the superclasses as well as the general call signature:
class Agent extends Events {
on(event: 'eventA', listener: (body: IAEvent) => void): this
on(event: 'eventB', listener: (body: IPayload<IBEvent>) => void):this;
on(event: 'eventC', listener: (body: ICEvent[]) => void): this;
on(event: string | symbol, listener: (...args: any[]) => void): this;
on(event: string | symbol, listener: (...args: any[]) => void): this {
return super.on(event, listener);
}
}
class MyAgent extends Agent {
on(event: 'eventA', listener: (body: IAEvent) => void): this
on(event: 'eventB', listener: (body: IPayload<IBEvent>) => void):this;
on(event: 'eventC', listener: (body: ICEvent[]) => void): this;
on(event: 'EventD', listener: (body: IEventD) => void):this;
on(event: string | symbol, listener: (...args: any[]) => void): this;
on(event: string | symbol, listener: (...args: any[]) => void): this {
return super.on(event, listener);
}
}
You can cut down the duplication a bit by defining an interface just for the on
method and extending it:
interface EventsOn<This> {
(event: string | symbol, listener: (...args: any[]) => void): This;
}
class Events {
on(event: string | symbol, listener: (...args: any[]) => void): this {
// ...
return this;
}
}
interface AgentOn<This> extends EventsOn<This> {
(event: 'eventA', listener: (body: IAEvent) => void): This;
(event: 'eventB', listener: (body: IPayload<IBEvent>) => void):This;
(event: 'eventC', listener: (body: ICEvent[]) => void): This;
}
interface Agent {
on: AgentOn<this>;
}
class Agent extends Events { }
interface MyAgentOn<This> extends AgentOn<This> {
(event: 'EventD', listener: (body: IEventD) => void):This;
}
interface MyAgent {
on: MyAgentOn<this>;
}
class MyAgent extends Agent { }
(When you declare an interface for a function type with call signatures, extending the interface seems to accumulate the call signatures, whereas when you declare an interface with an on
method, extending the interface seems to lose the call signatures from the superinterface.)
Upvotes: 1