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

问题描述 投票:0回答:3

我有泛型问题来推广事件处理类。

这是一些重现问题的原型代码:

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

typescript generics events
3个回答
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>可以赋值给Handler<E>(因为Handler<T>可以忽略额外的属性),而不是其他方式回合。

但TypeScript编译器有一个选项可以忽略这种函数类型的不兼容性 - 如果你关闭strictFunctionTypes它会认为函数是兼容的,如果它们的参数是这样或那样的兼容,无论方向如何。


3
投票

看起来你想要一个Manager来跟踪这个类的名称和那个类的处理程序之间的关系,在它的handlers属性中。如果没有,那么你可以忘记Managergo是通用的,只是到处使用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({});
  }

}

我已经将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>并完成它。

希望有所帮助;祝好运!


1
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.