'R' 可以用与 'Response<Command>' 无关的任意类型实例化

Ben*_*ght 14 compiler-errors typescript

好的,所以我正在尝试在 TypeScript 中实现一个简单的“命令总线”,但是我被泛型绊倒了,我想知道是否有人可以帮助我。这是我的代码:

这是命令总线的接口

export default interface CommandBus {
  execute: <C extends Command, R extends Response<C>>(command: C) => Promise<R>;
}
Run Code Online (Sandbox Code Playgroud)

这是实现

export default class AppCommandBus implements CommandBus {
  private readonly handlers: Handler<Command, Response<Command>>[];

  /* ... constructor ... */

  public async execute<C extends Command, R extends Response<C>>(
    command: C
  ): Promise<R> {
    const resolvedHandler = this.handlers.find(handler =>
      handler.canHandle(command)
    );

    /* ... check if undef and throw ... */

    return resolvedHandler.handle(command);
  }
}
Run Code Online (Sandbox Code Playgroud)

这是Handler界面的样子:

export default interface Handler<C extends Command, R extends Response<C>> {
  canHandle: (command: C) => boolean;
  handle: (command: C) => Promise<R>;
}
Run Code Online (Sandbox Code Playgroud)

Command是(当前)一个空接口,Response看起来像这样:

export default interface Response<C extends Command> {
  command: C;
}
Run Code Online (Sandbox Code Playgroud)

我收到了针对execute命令总线功能的最后一行的跟随编译错误错误,我完全被难住了。

type 'Response<Command>' is not assignable to type 'R'. 'R' could be instantiated with an arbitrary type which could be unrelated to 'Response<Command>'.

如果有人能够帮助我理解我做错了什么,我将永远感激不尽!

编辑

我意识到我可以通过类型转换来解决这个问题:

const resolvedHandler = (this.handlers.find(handler =>
  handler.canHandle(command)
) as unknown) as Handler<C, R> | undefined;
Run Code Online (Sandbox Code Playgroud)

但我仍然想知道如何解决这个双重演员。

jca*_*alz 24

TypeScript 中的泛型函数充当表示其泛型类型参数的所有可能规范的函数,因为指定类型参数的是函数的调用者,而不是实现者

type GenericFunction = <T>(x: T) => T;

const cantDoThis: GenericFunction = (x: string) => x.toUpperCase(); // error! 
// doesn't work for every T
cantDoThis({a: "oops"}); // caller chooses {a: string}: runtime error

const mustDoThis: GenericFunction = x => x; // okay, verifiably works for every T
mustDoThis({a: "okay"}); // okay, caller chooses {a: string}
Run Code Online (Sandbox Code Playgroud)

所以,让我们看看CommandBus

interface CommandBus {
  execute: <C extends Command, R extends Response<C>>(command: C) => Promise<R>;
}
Run Code Online (Sandbox Code Playgroud)

execute()方法CommandBus是一个泛型函数,声称能够接受调用者想要command的任何子类型的a Command(到目前为止可能还好),并返回 的值Promise<R>,其中R调用者想要的任何子类型Response<C>。这似乎不是任何人都可以合理实现的东西,并且大概您将始终必须断言您返回的响应是R调用者要求的响应。我怀疑这是你的意图。相反,这样的事情怎么样:

interface CommandBus {
    execute: <C extends Command>(command: C) => Promise<Response<C>>;
}
Run Code Online (Sandbox Code Playgroud)

这里,execute()只有一个泛型参数,C对应传入的类型command。并且返回值只是Promise<Response<C>>,而不是调用者要求的某些子类型。这更有可能实现,只要你有某种方式保证你有一个合适的处理程序C(比如,throw如果你不这样做,则通过ing。)


这将我们带到您的Handler界面:

interface Handler<C extends Command, R extends Response<C>> {
  canHandle: (command: C) => boolean;
  handle: (command: C) => Promise<R>;
}
Run Code Online (Sandbox Code Playgroud)

即使我们摆脱了试图表示Response<C>处理程序的特定子类型的暴政,也会产生如下结果:

interface Handler<C extends Command> {
  canHandle: (command: C) => boolean;
  handle: (command: C) => Promise<Response<C>>;
}
Run Code Online (Sandbox Code Playgroud)

我们仍然有问题canHandle()。这Handler就是它本身就是一个泛型类型的事实。泛型函数和泛型类型之间的区别与谁可以指定类型参数有关。对于函数,它是调用者。对于类型,它是实现者:

type GenericType<T> = (x: T) => T;

const cantDoThis: GenericType = (x: string) => x.toUpperCase(); // error! no such type

const mustDoThis: GenericType<string> = x => x.toUpperCase(); // okay, T is specified
mustDoThis({ a: "oops" }); // error! doesn't accept `{a: string}`
mustDoThis("okay");
Run Code Online (Sandbox Code Playgroud)

Handler<C>只想要handle()一个 type 的命令C,这很好。但是它的canHandle()方法要求命令是 type C,这太严格了。您希望来电者canHandle()选择C,并有返回值是 true还是false取决于whetehr的C调用者选择匹配由实施者选择的一个。为了在类型系统中表示这一点,我建议为根本不通用的父接口创建canHandle()一个通用的用户定义类型保护方法,如下所示:

interface SomeHandler {
    canHandle: <C extends Command>(command: C) => this is Handler<C>;
}

interface Handler<C extends Command> extends SomeHandler {
    handle: (command: C) => Promise<Response<C>>;
}
Run Code Online (Sandbox Code Playgroud)

所以,如果你有一个SomeHandler,你所能做的就是调用canHandle()。如果您向它传递一个类型为 的命令CcanHandle()返回true,则编译器将理解您的处理程序是 aHandler<C>并且您可以调用它。像这样:

function testHandler<C extends Command>(handler: SomeHandler, command: C) {
    handler.handle(command); // error!  no handle method known yet
    if (handler.canHandle(command)) {
        handler.handle(command); // okay!
    }
}
Run Code Online (Sandbox Code Playgroud)

我们快完成了。唯一令人费解的是,您正在使用SomeHandler[]find()方法来定位一个适合command. 编译器无法窥视回调handler => handler.canHandle(command)并推断回调是 type (handler: SomeHandler) => handler is SomeHandler<C>,因此我们必须通过对其进行注释来帮助它。那么编译器就会明白的返回值find()Handler<C> | undefined

class AppCommandBus implements CommandBus {
    private readonly handlers: SomeHandler[] = [];

    public async execute<C extends Command>(
        command: C
    ): Promise<Response<C>> {
        const resolvedHandler = this.handlers.find((handler): handler is Handler<C> =>
            handler.canHandle(command)
        );
        if (!resolvedHandler) throw new Error();
        return resolvedHandler.handle(command);
    }
}
Run Code Online (Sandbox Code Playgroud)

这与类型系统配合得很好,并且尽我所能。它可能适用于您的实际用例,也可能不起作用,但希望它为您提供有关如何有效使用泛型的一些想法。祝你好运!

Playground 链接到代码