简体   繁体   中英

Typescript: Typing an Implementation of an Interface Method defined as a generic Function Type Intersection, where Arguments share a base Type

TL;DR

Just look at the code blocks and you'll probably figure out the problem.

Link to Typescript Playground

Example Use Case

The aim is to specify an event emitter interface, which event emitter classes can implement and which can be used to safely type method calls to the emitter. As event managing basically is the same for different event emitter classes, a base emitter class shall be implemented that can be extended by other emitter classes.

In order to minimize redundancy and maximize convenience, event signatures are reduced to event names and listener signatures in one interface defined by the emitter class, passed to the emitter interface.

The following example illustrates the above described. For simplicity, the only functionality of the event emitter is binding a new listener through a method named on .

interface MyEvents
{
    foo(x: number): void;
    bar(): void;
    moo(a: string, b: Date): void;
}

export class MyEventEmitter implements EventEmitter<MyEvents>
{
    public on(event: 'foo', listener: (x: number) => void): this;
    public on(event: 'bar', listener: () => void): this;
    public on(event: 'moo', listener: (a: string, b: Date) => void): this;
    public on(_event: string, _listener: any): this
    { return this; }
}

Setup

// union to intersection converter from https://stackoverflow.com/a/50375286/2477364
type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

// helpers for the on signature from https://stackoverflow.com/a/50375712/2477364
type OnSignatures<ListenersT, ReturnT> =
    { [EventT in keyof ListenersT]: (event: EventT, listener: ListenersT[EventT]) => ReturnT };
type OnAll<ListenersT, ReturnT> =
    UnionToIntersection<OnSignatures<ListenersT, ReturnT>[keyof ListenersT]>;

// the actual event emitter interface
export interface EventEmitter<ListenersT>
{
    on: OnAll<ListenersT, this>;
}

This is becoming quite specific, but I thought about shrinking the example down, but I couldn't figure out a minimal example. Instead of neutralizing everything I thought it would be easier to follow with a use case.

Problem

Now to the actual problem. A class implementing base functionality of any event emitter that can be extended or mixed in by other event emitters.

export abstract class BaseEventEmitter<ListenersT> implements EventEmitter<ListenersT>
{
    public on<EventT extends keyof ListenersT>(_event: EventT, _listener: ListenersT[EventT]): this
    { return this; }
}

Typescript does not like my on method here:

Property 'on' in type 'BaseEventEmitter<ListenersT>' is not assignable to the same property in base type 'EventEmitter<ListenersT>'. Type '<EventT extends keyof ListenersT>(_event: EventT, _listener: ListenersT[EventT]) => this' is not assignable to type 'UnionToIntersection<OnSignatures<ListenersT, this>[keyof ListenersT]>'.

Unfortunately created a generic class that implements the interface in a simple way is not going to be possible.

If the class is not generic, the compiler will be able to rezolve the signature of on and perform the required checks. So for example this works:

export class MyEventEmitter implements EventEmitter<MyEvents>
{
    public on(event: keyof MyEvents, listener: MyEvents[keyof MyEvents]): this {
        return this;        
    }
}

The moment we have a generic type parameter, the compiler will not be able to resolve the conditional type, and what it sees is that you are trying to assign a function to a field of some weird conditional type and will decide that since it can't check this it is not safe.

You can create an intermediate protected method that you will assign to on but you will need a type assertion:

export abstract class BaseEventEmitter<ListenersT> implements EventEmitter<ListenersT>
{
    protected onInternal(event: keyof ListenersT, listener: ListenersT[keyof ListenersT]): this {
        return this;        
    }
    public on: EventEmitter<ListenersT>['on'] = this.onInternal as any;
}

export class MyEventEmitter extends BaseEventEmitter<MyEvents>
{
}

Or you can declare your class and assign it a constant typed as the constructor to a class that returns an EventEmitter

export abstract class BaseEventEmitterImpl<ListenersT>
{
    public on(event: keyof ListenersT, listener: ListenersT[keyof ListenersT]): this {
        return this;        
    }
}

const BaseEventEmitter: new <T>() => EventEmitter<T> & BaseEventEmitterImpl<T> = BaseEventEmitterImpl as any;

export class MyEventEmitter extends BaseEventEmitter<MyEvents>
{
}

new MyEventEmitter().on('foo', x => 1);

Neither solution is ideal, but creating a derived class will work mostly as expected (unless you want to override on as in the first case you need to override onInternal ), and the clients of the derived classes will not be the wiser and have types for the callbacks.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM