Node: 17.7.1

Typescript: 4.6.3

我在DDD上使用一个旧的回购协议,在try 重新创建代码时遇到了一个Typescript错误,我不知道如何修复.

使用以下代码注册时,AfterSomethingCreated类中出现IDE"错误":

Argument of type '(event: Event) => Promise<void>' is not assignable to parameter of type '(event: IEvent) => void'.
  Types of parameters 'event' and 'event' are incompatible.
    Property 'something' is missing in type 'IEvent' but required in type 'SomethingCreatedEvent'.ts(2345)

类SomethingCreatedEvent实现IEvent接口.SomethingCreatedEvent除了来自IEvent的属性之外,还包括一个属性.当包含属性时,抛出错误;当取出属性时,在IDE中抛出上述错误

代码:

IEvent.ts

export interface IEvent {
    //.....
}

IHandle.ts

export interface IHandle<IEvent> {
  setupSubscriptions(): void;
}

Events.ts

export class Events {
    
    //Methods...

    public static register(callback: (event: IEvent) => void, eventClassName: string): void {
        //Do Stuff
    }

    //Methods...

 }

SomethingCreatedEvent.ts

export class SomethingCreatedEvent implements IEvent {
  //.....
  public something: Something;

  constructor (something: Something) {
    this.something = Something;
    //.....
  }
  //......

  }
}

AfterSomethingCreated (Where Error Is Occurring)

export class AfterSomethingCreated implements IHandle<SomethingCreatedEvent> {
  constructor () {
    this.setupSubscriptions();
  }

  setupSubscriptions(): void {
    ---> ERROR -> Events.register(this.onSomethingCreatedEvent.bind(this), SomethingCreatedEvent.name);
  }
  private async onSomethingCreatedEvent (event: SomethingCreatedEvent): Promise<void> {
    //Do stuff
  }
}

推荐答案

发生错误的原因是Events.register()接受一个回调,该回调应该接受任何IEvent.因此,用尽可能少的IEvent调用回调应该是完全可以接受的(在您的例子中,因为IEvent是一个空接口,所以这只是{},空对象):

public static register(callback: (event: IEvent) => void, eventClassName: string): void {
    callback({}); // <-- look, no error
}

另一方面,onSomethingCreatedEvent()方法希望它的输入是SomethingCreatedEvent,所以这个方法访问SomethingCreatedEvent个对象特有的事件属性应该是完全可以接受的,比如something属性(我假设它的值是string,因为你没有在代码中定义Something类型.也就是说,我的行为就像type Something = string;):

private async onSomethingCreatedEvent(event: SomethingCreatedEvent): Promise<void> {
    console.log(event.something.toUpperCase());
}

但现在,在setupSubscriptions()中,您正在向Events.register()传递一个只接受SomethingCreatedEvent个事件的回调,这是一个错误:

setupSubscriptions(): void {
    Events.register(this.onSomethingCreatedEvent.bind(this), SomethingCreatedEvent.name); // error!
    // -----------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // Argument of type '(event: SomethingCreatedEvent) => Promise<void>' is not
    // assignable to parameter of type '(event: IEvent) => void'.
}

这是一个有充分理由的错误.如果按上面修改的方式调用代码,则会出现运行时错误,因为我们调用的回调输入错误:

new AfterSomethingCreated();
// RUNTIME ERROR: Uncaught (in promise) TypeError: event.something is undefined

因为这是现有的代码,所以这在实践中可能不会发生.实际上,现有的一些JavaScript在技术上是不安全的.TypeScript以bivariant的方式判断method parameters,这意味着它将允许安全的缩小和不安全的扩大操作.Function个参数的判断更加严格(假设启用了the --strictFunctionTypes compiler option个参数,这是the --strict suite of compiler features的一部分).

如果想要获得更松散的类型行为,需要将callback的类型表示为method,而不是function.顺便说一下,区别在于:

interface Test {
    functionSyntax: (ev: IEvent) => void;
    methodSyntax(ev: IEvent): void;
}
const test: Test = {
    functionSyntax: (ev: SomethingCreatedEvent) => { }, // error!
    methodSyntax: (ev: SomethingCreatedEvent) => { } // okay!
}

看看TestfunctionSyntax的声明如何是一个带有箭头函数表达式类型的属性,而methodSyntax看起来更像是一个方法声明.并看看如何实施test个投诉关于functionSyntax个属性接受太窄的一种类型,而methodSyntax个属性不投诉.

因此,如果您只想 suppress 错误,可以依赖方法语法.这有点棘手,因为独立函数没有方法语法.你不能把(ev: IEvent): void写成一个类型,而{(ev: IEvent): void}被当作函数语法.这里的技巧是创建一个实际的方法类型,然后在周围的对象中创建index:

type MethodSyntax = { method(event: IEvent): void }["method"]
// type MethodSyntax = (event: IEvent) => void, but marked as a method

现在如果你用这个写Events.register():

public static register(callback: MethodSyntax, eventClassName: string): void { }

然后你的电话会突然正常工作:

Events.register(this.onSomethingCreatedEvent.bind(this), SomethingCreatedEvent.name); // okay

这不再是类型安全的,但至少它没有错误.


如果您关心强制执行类型安全,那么您可能需要重构,以便在处理程序处理回调时不会发生任何不好的事情.以下是一种可能的方法:

class Events {
    static handlers: ((event: IEvent) => void)[] = [];
    public static register<T extends IEvent>(
        callback: (event: T) => void,
        eventClass: new (...args: any) => T
    ): void {
        this.handlers.push(ev => ev instanceof eventClass && callback(ev));
    }
    public static handleEvent(event: IEvent) {
        this.handlers.forEach(h => h(event));
    }
}

现在Events.register()是一个generic函数,它接受一个只接受T类型事件的callback,以及一个eventClass构造函数(而不是类name).这样就可以 for each 事件调用每个处理程序...除非event instanceof eventClass,否则我们不会打callback(event).只有类name,编译器很难验证任何特定事件是否适合任何特定回调,因为类的name属性在TypeScript中不是强类型的(有关更多信息,请参阅microsoft/TypeScript#43325和内部链接的问题).

然后现在SomethingCreatedEvent美元接受以下条件:

    setupSubscriptions(): void {
        Events.register(this.onSomethingCreatedEvent.bind(this), SomethingCreatedEvent);
    }

虽然有些不合适的东西会被标记:

Events.register((o: SomethingCreatedEvent) => { }, Date) // error!
// Property 'something' is missing in type 'Date' but required in type 'SomethingCreatedEvent'.

Playground link to code

Node.js相关问答推荐

如何模拟 mysql2 `getConnection`

如何从mongoose 对象内嵌套的数组中提取数组元素?

密码加密的最佳实践是什么?

mongoose findOneAndUpdate 不更新数据库

使用 Nodejs 获取 Firebase 云消息传递历史记录

suppress AWS SDK v2 弃用消息

如何修改这个flake.nix,这样我就不用每次加载环境都加载nix包了

node-gyp: "..\src\binding.cc: 没有这样的文件或目录"

Express.js cookie setter 的 Domain 属性:如何与 *多个 * 域共享 cookie?

mongoose.model() 方法返回未定义

错误:无法检测到网络(event="noNetwork",code=NETWORK_ERROR,version=providers/5.6.8)

Winston http 日志(log)级别的行为与 info 不同

tsc:当我上传 React+next js 和 node 项目时,在 heroku 找不到

使用 Socket.io 将客户端连接到服务器

在 Node.js 与 Cron 作业(job)中设置间隔?

AngularJS +sails.js

向 Stripe 提交付款请求时出现没有此类令牌错误

使用 node.js 循环 JSON

找不到在 docker compose 环境中运行的 node js 应用程序的模块

如何设置 useMongoClient (Mongoose 4.11.0)?