Typescript泛型类型约束和类型兼容性

时间:2018-08-07 19:16:22

标签: typescript generics events

我在泛型方面遇到问题,无法对事件处理类进行泛化。

以下是一些重现此问题的原始代码:

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

3 个答案:

答案 0 :(得分:4)

  

在这种情况下,有没有什么办法可以暗示编译器在T和E处是兼容的

编译器已经知道TE是兼容的,因为E extends T函数具有go约束。更准确地说,E可分配给T

但是它不会使Handler<E>Handler<T>兼容,因为ET是处理程序参数的类型。假设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属性内跟踪类名和该类的处理程序之间的关系。如果不是这样,那么您可以忘记Managergo是通用的,而只需在各处使用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({});
  }

}

我已经将handlersMap更改为常规对象,因为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的类型很强,足以知道它有两个成员FooBar,它们对应于类型的处理程序FooBar

这里显然有很多警告……一个是TypeScript不知道构造函数的name属性只是一个string,我们需要缩小它的范围。而且您必须记住要使用链接。每次调用Manager都会得到一个新的go()对象(可以通过类型声明来解决,这些声明有其自己的警告)。

无论如何,关键是:您需要相当多的参与才能在类型级别跟踪处理程序。除非您需要它,否则可能要使用Handler<any>并完成它。

希望有所帮助;祝你好运!

答案 2 :(得分:1)

如果我正确理解了您的意图,则地图的每个值并不是真正的Handler<T>,而是Handler<the subclass of T indicated by the key>。最好的选择是将地图的值类型声明为Handler<any>