我在泛型方面遇到问题,无法对事件处理类进行泛化。
以下是一些重现此问题的原始代码:
interface Base { }
interface ClassOf<T extends Base> extends Function { new (...args: any[]): T; }
type Handler<T extends Base = Base> = (event: T) => Promise<any>;
export class Manager<T extends Base = Base> {
public handlers: Map<string, Handler<T>> = new Map();
go<E extends T>(first: ClassOf<E>, handler: Handler<E>): void {
this.handlers.set(first.name, handler);
^^^^^^^
}
}
Typescript编译器返回以下错误:
[ts]
Argument of type 'Handler<E>' is not assignable to parameter of type 'Handler<T>'.
Types of parameters 'event' and 'event' are incompatible.
Type 'T' is not assignable to type 'E'.
Type 'Base' is not assignable to type 'E'.
(parameter) handler: Handler<E>
比使Handler函数签名采用“ any”还短,在这种情况下,是否有任何办法在T和E处暗示编译器是否兼容?
还有其他途径可以使此类尽可能通用吗?
任何指针表示赞赏!
-b
答案 0 :(得分:4)
在这种情况下,有没有什么办法可以暗示编译器在T和E处是兼容的
编译器已经知道T
和E
是兼容的,因为E extends T
函数具有go
约束。更准确地说,E
可分配给T
。
但是它不会使Handler<E>
与Handler<T>
兼容,因为E
和T
是处理程序参数的类型。假设E
是一个具有两个属性的对象
type E = {firstName: string; lastName: string}
和T
只有firstName
:
type T = {firstName: string};
假设您有一个E
处理程序,该处理程序希望接收具有两个属性的对象
function handlerForE(e: {firstName: string, lastName: string});
那么您就不能将其分配给Handler<T>
,因为可以用Handler<T>
-T
中只有一个字段的对象调用firstName
,和Handler<E>
都需要。
基本上,类型兼容性通常沿特定方向发展,将类型放置在函数参数位置会切换其方向-如果E
可分配给T
,则Handler<T>
可分配给{ {1}}(因为Handler<E>
可以忽略额外的属性),而不是相反。
但是TypeScript编译器可以选择忽略函数类型的不兼容性-如果关闭strictFunctionTypes
,则无论参数的方向如何,无论其方向如何,它都会认为函数兼容。
答案 1 :(得分:3)
似乎您希望Manager
在其handlers
属性内跟踪类名和该类的处理程序之间的关系。如果不是这样,那么您可以忘记Manager
和go
是通用的,而只需在各处使用Handler<any>
。
如果是这样,那将不是那么简单。问题在于,每次调用go()
并添加处理程序时,都需要更改 Manager
的类型以反映出来。具体来说,您想缩小handlers
的类型。 TypeScript不支持更改现有变量的类型,因此您不能直接这样做。您可以 使用builder pattern,以便对go
的调用返回类型适当缩小的 new Manager
,如下所示:
export class Manager<M extends Record<keyof M, Handler<any>>> {
constructor(public handlers: M) {
}
go<K extends string, E extends Base>(
first: {
name: K & (K extends keyof M ? never : K),
new(...args: any[]): E
},
handler: Handler<E>
) {
const handlers = Object.assign(
{ [first.name]: handler } as Record<K, Handler<E>>,
this.handlers
);
return new Manager(handlers);
}
static make(): Manager<{}> {
return new Manager({});
}
}
我已经将handlers
从Map
更改为常规对象,因为TypeScript并不能真正地维护Map
中不同的键/值类型,而它却适用于对象,并且因为如果您的密钥只是一个string
或字符串文字,那么Map
可能会过分杀人。
然后您可以像这样使用它:
class Foo implements Base {
// @ts-ignore: TypeScript doesn't realize that Foo.name is "Foo",
// so we will force it here
static name: "Foo";
foo: string = "foo";
}
const fooHandler: Handler<Foo> = (event: Foo) => fetch(event.foo);
class Bar implements Base {
// @ts-ignore: TypeScript doesn't realize that Bar.name is "Bar",
// so we will force it here
static name: "Bar";
bar: string = "bar";
}
const barHandler: Handler<Bar> = (event: Bar) => fetch(event.bar);
const manager = Manager.make().go(Foo, fooHandler).go(Bar, barHandler);
manager.handlers.Foo // known to be Handler<Foo>
manager.handlers.Bar // known to be Handler<Bar>
可行...您可以看到manager.handlers
的类型很强,足以知道它有两个成员Foo
和Bar
,它们对应于类型的处理程序Foo
和Bar
。
这里显然有很多警告……一个是TypeScript不知道构造函数的name
属性只是一个string
,我们需要缩小它的范围。而且您必须记住要使用链接。每次调用Manager
都会得到一个新的go()
对象(可以通过类型声明来解决,这些声明有其自己的警告)。
无论如何,关键是:您需要相当多的参与才能在类型级别跟踪处理程序。除非您需要它,否则可能要使用Handler<any>
并完成它。
希望有所帮助;祝你好运!
答案 2 :(得分:1)
如果我正确理解了您的意图,则地图的每个值并不是真正的Handler<T>
,而是Handler<the subclass of T indicated by the key>
。最好的选择是将地图的值类型声明为Handler<any>
。