【问题标题】:Extensible, strongly typed Event Emitter Interface in TypescriptTypescript 中可扩展的强类型事件发射器接口
【发布时间】:2019-09-09 20:01:23
【问题描述】:

我在 Typescript 中使用强类型事件发射器接口已经有一段时间了,但现在我需要它来支持向其添加自己的事件的子类。在某些时候,Typescript 无法识别基类事件。

这是精简版的代码 (Playground Link):

type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type AddParameters<ListenersT, EventT> =
    ListenersT extends (...args: infer ArgsT) => void
        ? (event: EventT, ...args: ArgsT) => Promise<boolean>
        : never;

type EmitSignatures<ListenersT> =
    { [EventT in keyof ListenersT]: AddParameters<ListenersT[EventT], EventT> };
type EmitAll<ListenersT> = UnionToIntersection<EmitSignatures<ListenersT>[keyof ListenersT]>

type OnSignatures<ListenersT, ReturnT> =
    { [EventT in keyof ListenersT]: (event: EventT, listener: ListenersT[EventT]) => ReturnT };
type OnAll<ListenersT, ReturnT> =
    UnionToIntersection<OnSignatures<ListenersT, ReturnT>[keyof ListenersT]>;

type EventEmitter<ListenersT> = EmitterInterface<ListenersT>;

export interface EmitterInterface<ListenersT>
{
    emit: EmitAll<ListenersT>;
    on: OnAll<ListenersT, this>;
}

/////////////////////////////////////////////////////////////////////////////////////////////

interface VehicleEvents
{
    accelerate(acceleration: number): void;
    brake(deceleration: number): void;
}

interface BusEvents extends VehicleEvents
{
    doorStateChange(front: boolean, middle: boolean, rear: boolean): void
}

interface Vehicle<E extends VehicleEvents> extends EventEmitter<E>
{
    onSig: OnSignatures<E, this>;
    onSigs: OnSignatures<E, this>[keyof E];
}

class Vehicle<E extends VehicleEvents>
{
    public constructor()
    { this.on('brake', () => this.flashBrakeLights()); } // supposed to work?

    public flashBrakeLights(): void {}

    public hitTheGas(strength: number): void
    { this.emit('accelerate', strength * 42); } // supposed to work?

    public test(): void
    {
        this.onSig.accelerate;
        this.onSig.brake;
        this.onSigs('accelerate', (a) => undefined); // error I don't understand
        this.onSigs('brake', (d) => undefined); // error I don't understand
        this.onSigs('foo', () => undefined); // supposed to error
    }
}

interface Bus extends EventEmitter<BusEvents> {}

class Bus extends Vehicle<BusEvents>
{
    public doorState: [boolean, boolean, boolean] = [false, false, false];

    public constructor()
    {
        super();
        this.on('accelerate', () => {
            this.door(0, false);
            this.door(1, false);
            this.door(2, false);
        });
    }

    public door(index: number, state: boolean): void
    {
        this.doorState[index] = state;
        this.emit('doorStateChange', ...this.doorState);
    }
}

export const bus = new Bus();

E 类型被声明为 VehicleEvents 的扩展,这应该足以让 Typescript 知道有 acceleratebrake 事件,不是吗?

任何解释为什么这不起作用?有关如何解决此问题或以其他方式实现我需要的任何想法?

【问题讨论】:

  • Vehicle 内部,问题是E 是未知的,所以所有条件和映射类型的魔法都不能完全解决,所以你会得到错误。此外,onSigs 是签名的联合,因此即使从外部也不会像您期望的那样工作(参数必须是所有可能性的交集。
  • 是的 EVehicle 内部是未知的,但它知道它扩展了 VehicleEvents。所以我认为 Typescript 甚至没有尽可能地解决它。考虑到onSigs,我也想到了这一点,但是第一个参数不应该是所有事件名称的联合,因此也会出错(它只抱怨我的处理程序部分)?另外,有什么想法可以实现我的需要吗? :(
  • 我会考虑的 .. 我刚才回答了一个类似的问题 stackoverflow.com/questions/50369299/…,但它也不是可扩展的 .. 我尝试了几件事,但没有让它工作不幸的是你想要的方式
  • sub-events 是一个完全可扩展的替代方案;)如果您想了解详细信息,请询问,我会发布详细答案。

标签: typescript


【解决方案1】:

问题是在类内部,如果那些花哨的条件类型(不确定它们来自cough)仍然包含未解析的类型参数,则无法解析它们。因此,虽然您使用泛型类型参数进行可扩展性的方法似乎是一个好主意,但效果是它使 onemit 在类中不可用。

一种解决方案是不使用类型参数,只使用事件接口本身。这样做的问题(正如您无疑发现的那样)是它使类无法扩展,因为onemit 的任何派生版本都将与基类型版本不兼容。

要解决此问题,我们可以使用从基本类型中删除 onemit 的函数。这有点骇人听闻,但我认为没有更好的方法。

interface VehicleEvents {
  accelerate(acceleration: number): void;
  brake(deceleration: number): void;
}

interface BusEvents extends VehicleEvents {
  doorStateChange(front: boolean, middle: boolean, rear: boolean): void
}

interface Vehicle extends EventEmitter<VehicleEvents> {}

class Vehicle {
  public constructor() {
    this.on('brake', () => this.flashBrakeLights()); //ok 
  }

  public flashBrakeLights(): void { }

  public hitTheGas(strength: number): void { this.emit('accelerate', strength * 42); } // ok

}

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
interface Bus extends EventEmitter<BusEvents> { }

function extendEmitter<TBaseCtor extends new (...a: any[])=> any>(ctor: TBaseCtor){
  return ctor as (new (...a: ConstructorParameters<TBaseCtor>) => Omit<InstanceType<TBaseCtor>, 'on' | 'emit'>)
}
class Bus extends extendEmitter(Vehicle) {
  public doorState: [boolean, boolean, boolean] = [false, false, false];

  public constructor() {
    super();
    this.on('accelerate', () => {
      this.door(0, false);
      this.door(1, false);
      this.door(2, false);
    });
  }

  public door(index: number, state: boolean): void {
    this.doorState[index] = state;
    this.emit('doorStateChange', ...this.doorState);
  }

}

export const bus = new Bus();

上述版本并不能确保新派生的事件适当地实现了基础事件。我们可以编写一个版本来验证这一点,但它需要对原始定义进行小改动,以允许我们从基本类型中提取事件接口:

type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type AddParameters<ListenersT, EventT> =
    ListenersT extends (...args: infer ArgsT) => void
        ? (event: EventT, ...args: ArgsT) => Promise<boolean>
        : never;

type EmitSignatures<ListenersT> =
    { [EventT in keyof ListenersT]: AddParameters<ListenersT[EventT], EventT> };
type EmitAll<ListenersT> = UnionToIntersection<EmitSignatures<ListenersT>[keyof ListenersT]>

type OnSignatures<ListenersT, ReturnT> =
    { [EventT in keyof ListenersT]: (event: EventT, listener: ListenersT[EventT]) => ReturnT };
type OnAll<ListenersT, ReturnT> =
    UnionToIntersection<OnSignatures<ListenersT, ReturnT>[keyof ListenersT]>;

type EventEmitter<ListenersT> = EmitterInterface<ListenersT>;

export interface EmitterInterface<ListenersT>
{
    emit: EmitAll<ListenersT>;
    on: OnAll<ListenersT, this> & {__source: ListenersT}; // do not use __source, just here to allow EventTypes to work
}

type EventTypes<T> = T extends EventEmitter<infer U> ? U : never;

/////////////////////////////////////////////////////////////////////////////////////////////

interface VehicleEvents {
  accelerate(acceleration: number): void;
  brake(deceleration: number): void;
}

interface BusEvents extends VehicleEvents {
  doorStateChange(front: boolean, middle: boolean, rear: boolean): void
}

interface Vehicle extends EventEmitter<VehicleEvents> {}

class Vehicle {
  public constructor() {
    this.on('brake', () => this.flashBrakeLights()); //ok 
  }

  public flashBrakeLights(): void { }

  public hitTheGas(strength: number): void { this.emit('accelerate', strength * 42); } // ok

}

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
interface Bus extends EventEmitter<BusEvents> { }


function extendEmitter<TBaseCtor extends new (...a: any[])=> any>(ctor: TBaseCtor){
  return function<TEvents extends EventTypes<InstanceType<TBaseCtor>>>(){
    return ctor as (new (...a: ConstructorParameters<TBaseCtor>) => Omit<InstanceType<TBaseCtor>, 'on' | 'emit'> & EventEmitter<TEvents>)
  }
}

class Bus extends extendEmitter(Vehicle)<BusEvents>() {
  public doorState: [boolean, boolean, boolean] = [false, false, false];

  public constructor() {
    super();
    this.on('accelerate', () => {
      this.door(0, false);
      this.door(1, false);
      this.door(2, false);
    });
  }

  public door(index: number, state: boolean): void {
    this.doorState[index] = state;
    this.emit('doorStateChange', ...this.doorState);
  }

}

export const bus = new Bus();

【讨论】:

  • 非常感谢您找到解决方案。不过第二部分没看懂仅使用第一个代码块中的更改即可在我的测试中使用。也许这是因为我们的实现不同?
  • @Neon 也许,如果你提供一个带有代码的仓库,我可以看看。如果您需要更多信息,我们也可以在 gitter 上聊天,这是一个有趣的话题 :)
猜你喜欢
  • 2021-08-07
  • 2015-04-01
  • 1970-01-01
  • 2020-05-04
  • 2022-12-06
  • 2018-04-09
  • 2017-10-21
  • 2018-06-20
  • 1970-01-01
相关资源
最近更新 更多