首页 > 解决方案 > “R”可以用与“响应”无关的任意类型实例化'

问题描述

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

这是命令总线的接口

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

这是实现

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

这就是Handler界面的样子:

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

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

export default interface Response<C extends Command> {
  command: C;
}

对于命令总线函数的最后一行,我收到以下编译错误错误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;

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

标签: typescriptcompiler-errors

解决方案


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}

那么,让我们看看CommandBus

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

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

interface CommandBus {
    execute: <C extends Command>(command: C) => Promise<Response<C>>;
}

这里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>;
}

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

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

我们仍然有问题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");

您只想Handler<C>使用handle()type 的命令C,这很好。但是它的canHandle()方法要求命令是 type C,这太严格了。您希望调用者选择canHandle()C并且返回值是 truefalse取决于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>>;
}

所以,如果你有一个SomeHandler,你所能做的就是打电话canHandle()。如果你向它传递一个 type 的命令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!
    }
}

我们快完成了。唯一需要注意的是,您正在使用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);
    }
}

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

Playground 代码链接


推荐阅读