我有泛型问题来推广事件处理类。
这是一些重现问题的原型代码:
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
有没有办法提示在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>
可以赋值给Handler<E>
(因为Handler<T>
可以忽略额外的属性),而不是其他方式回合。
但TypeScript编译器有一个选项可以忽略这种函数类型的不兼容性 - 如果你关闭strictFunctionTypes
它会认为函数是兼容的,如果它们的参数是这样或那样的兼容,无论方向如何。
看起来你想要一个Manager
来跟踪这个类的名称和那个类的处理程序之间的关系,在它的handlers
属性中。如果没有,那么你可以忘记Manager
和go
是通用的,只是到处使用Handler<any>
。
如果是这样,那就不是那么简单了。问题是,每次调用go()
并添加处理程序时,都需要更改Manager
的类型以反映它。具体来说,你想缩小handlers
的类型。 TypeScript不支持更改现有变量的类型,因此您无法直接执行此操作。您可以使用builder pattern,以便调用go
返回一个新的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>
并完成它。
希望有所帮助;祝好运!
如果我理解你的意图,你的地图的每个值都不是真正的Handler<T>
而是Handler<the subclass of T indicated by the key>
。您最好的选择可能是将地图的值类型声明为Handler<any>
。