好,所以我试图在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;
}
我在commandbus的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;
但是我仍然想知道如何解决这个双重问题。
答案 0 :(得分:6)
TypeScript中的泛型函数充当表示其泛型类型参数的每个可能规范的函数,因为指定类型参数的是函数的 caller ,而不是 implementer :
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()
的{{1}}方法是一个通用函数,声称能够接受呼叫者想要的CommandBus
的任何子类型的command
(到目前为止,可能),并返回值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
界面:
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()
本身是通用的 type 的事实。通用功能和通用 types 之间的区别与谁可以指定类型参数有关。对于函数,它是调用者。对于类型,它是实现者:
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()
的命令。但是它的C
方法 也要求该命令的类型为canHandle()
,这太严格了。您希望C
的 caller 选择canHandle()
,并根据{{1}的不同将返回值设为C
或true
。 }}由调用者选择,与实现者选择的匹配。为了在类型系统中表示这一点,我建议使false
成为父接口的通用user-defined type guard方法,而该父接口根本不是通用的,就像这样:
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()
返回C
,则编译器将理解您的处理程序为canHandle()
,您可以调用它。像这样:
true
我们快完成了。唯一奇怪的是,您正在使用Handler<C>
的{{1}}方法来找到适合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)
:
(handler: SomeHandler) => handler is SomeHandler<C>
这在类型系统上很好地工作,并且尽我所能。它可能不适用于您的实际用例,但希望它能为您提供一些有关如何有效使用泛型的想法。祝你好运!