如何在typescript抽象类中设置正确的类型?

2sbarzqh  于 2022-11-26  发布在  TypeScript
关注(0)|答案(1)|浏览(133)

我在抽象类中设置正确类型时遇到问题。我想做如下操作:

abstract class AbstractMap<TTile extends AbstractTile> {
  public readonly fields: TTile[];

  constructor() {
    this.fields = [this.createTile(0,0), this.createTile(0,1)];
  }

  protected abstract createTile(x: number, y: number): TTile;
}

abstract class AbstractTile {
  protected abstract readonly map: AbstractMap<this>;

  protected constructor(public readonly x: number, public readonly y: number) {
  }

  public get neighbors(): this[] {
    return this.map.fields.filter(f => f);
  }
}

class ServerMap extends AbstractMap<ServerTile> {
  protected createTile(x: number, y: number): ServerTile {
    return new ServerTile(x, y, this);
  }
}

class ServerTile extends AbstractTile {
  protected readonly map: ServerMap;

  constructor(x: number, y: number, map: ServerMap) {
    super(x, y);
    this.map = map;
  }
}

我尝试在ServerTile.map属性上获取 ServerMap 类型,但一直都有错误:

  Type 'ServerMap' is not assignable to type 'AbstractMap<this>'.
     Types of property 'fields' are incompatible.
       Type 'ServerTile[]' is not assignable to type 'this[]'.
         Type 'ServerTile' is not assignable to type 'this'.
           'ServerTile' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'ServerTile'.

我试图在AbstractTile类中设置AbstractMap<AbstractTile>而不是AbstractMap<this>,但随后我需要在AbstractTile.neighbors函数上设置AbstractTile[]返回类型。

mklgxw1f

mklgxw1f1#

多态this类型是一个隐式F绑定类型。它的作用就像一个被约束到当前类型的generic类型参数。和泛型一样,指定类型参数比编写将其视为未解析参数的代码更容易。在AbstractTile及其后代类的类体内,我们只知道this是当前类的某个子类型。但我们不知道是哪一个。这意味着你写的很多代码在技术上可能是不安全的。例如,对于你的代码,有人可能会写:

class Oops extends ServerTile { prop = "oops" }

const oopses = new Oops(1, 2, new ServerMap()).neighbors;
// const oopses: Oops[]

编译器认为oopses的类型是Oops[],因为这是neighbors的假定类型。但是ServerMap的实现并不是这样工作的,所以如果你继续,你会得到一个运行时错误:

oopses.forEach(o => o.prop.toUpperCase()) // compiles okay, but
// 💥 RUNTIME ERROR! o.prop is undefined

所以编译器给出的错误消息,'ServerTile' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'ServerTile'告诉你问题到底出在哪里。
当然,你可能认为这样的事情不太可能发生,所以你想使用this类型。这是你的特权,你可以向编译器Assert这不是一个问题:

class ServerTile extends AbstractTile {
  protected readonly map: AbstractMap<this>;
  constructor(x: number, y: number, map: ServerMap) {
    super(x, y);
    // assert
    this.map = map as AbstractMap<AbstractTile> as AbstractMap<this>;
  }
}

这会抑制编译器错误,现在您有责任确保对于您将遇到的所有this类型,ServerMap的行为都与AbstractMap<this>相同。
如果你真的想用一种更可靠的类型安全的方式来编写这个函数,你可以用隐式的F绑定类型参数this来替换 * 显式的F绑定类型参数T,形式是T extends F<T>,在你的例子中是class AbstractTile<T extends AbstractTile<T>>

abstract class AbstractMap<T extends AbstractTile<T>> {
  public readonly fields: T[];

  constructor() {
    this.fields = [this.createTile(0, 0), this.createTile(0, 1)];
  }

  protected abstract createTile(x: number, y: number): T;
}

abstract class AbstractTile<T extends AbstractTile<T>> {
  protected abstract readonly map: AbstractMap<T>;

  protected constructor(public readonly x: number, public readonly y: number) {
  }

  public get neighbors(): T[] {
    return this.map.fields.filter(f => f);
  }
}

现在,您可以编写ServerMapServerTile,如下所示:

class ServerMap extends AbstractMap<ServerTile> {
  protected createTile(x: number, y: number): ServerTile {
    return new ServerTile(x, y, this);
  }
}

class ServerTile extends AbstractTile<ServerTile> {
  protected readonly map: ServerMap;

  constructor(x: number, y: number, map: ServerMap) {
    super(x, y);
    this.map = map;
  }
}

所有这些都可以按照需要编译,并且不再可能出现之前的失败模式,因为现在neighbors属性的类型是ServerTile[],而不是Oops[]

class Oops extends ServerTile { prop = "oops" }

const oopses = new Oops(1, 2, new ServerMap()).neighbors;
// const oopses: ServerTile[]

因此,如果您尝试引用oopses[0]prop属性,编译器将发出警告。
Playground代码链接

相关问题