首页 > 解决方案 > 打字稿:键入定义为通用函数类型交集的接口方法的实现,其中参数共享一个基本类型

问题描述

TL;博士

只需查看代码块,您可能就会发现问题所在。

链接到 Typescript Playground

示例用例

目的是指定一个事件发射器接口,哪些事件发射器类可以实现,哪些可以用于安全地键入对发射器的方法调用。由于不同事件发射器类的事件管理基本相同,因此应实现一个基发射器类,该基发射器类可以由其他发射器类扩展。

为了最小化冗余和最大化便利性,事件签名被简化为事件名称和侦听器签名,在一个由发射器类定义的接口中传递给发射器接口。

以下示例说明了上述内容。为简单起见,事件发射器的唯一功能是通过名为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; }
}

设置

// 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>;
}

这变得非常具体,但我考虑过缩小示例,但我想不出一个最小的示例。而不是中和所有我认为用用例更容易遵循的东西。

问题

现在到实际问题。一个实现任何事件发射器的基本功能的类,可以由其他事件发射器扩展或混合。

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

打字稿不喜欢我的on方法:

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]>'.

标签: typescript

解决方案


不幸的是,创建一个以简单方式实现接口的泛型类是不可能的。

如果该类不是通用的,编译器将能够解析签名on并执行所需的检查。因此,例如,这有效:

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

当我们有一个泛型类型参数时,编译器将无法解析条件类型,它看到的是你试图将一个函数分配给一个奇怪的条件类型的字段,并且会决定,因为它可以' t 检查它是不安全的。

您可以创建一个您将分配给的中间受保护方法,on但您需要一个类型断言:

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>
{
}

或者您可以声明您的类并将其分配给一个类型为构造函数的常量,该类返回一个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);

这两种解决方案都不是理想的,但是创建派生类将大部分按预期工作(除非您想像on在第一种情况下需要 override 那样覆盖onInternal),并且派生类的客户端不会更聪明并且具有回调类型.


推荐阅读