首页 > 解决方案 > 为什么通用约束会产生 TS2417 错误?

问题描述

我在实现以下代码的 Table 类上收到 TypeScript 2417 错误:

export abstract class BaseTable {
  protected constructor() {}

  static Builder = class<T extends BaseTable> {
    protected constructor() {}

    build(): T {
      return this.buildTable();
    }

    protected buildTable(): T {
      throw new Error("Must be implemented in subclasses");
    }
  };
}

export class Table extends BaseTable {
  private constructor() {
    super();
  }

  static Builder = class extends BaseTable.Builder<Table> {
    protected constructor() {
      super();
    }

    static create() {
      return new Table.Builder();
    }

    protected buildTable(): Table {
      return new Table();
    }
  };
}

这会产生错误

Class static side 'typeof Table' incorrectly extends base class static side 'typeof BaseTable'.
  The types returned by '(new Builder()).build()' are incompatible between these types.
    Type 'Table' is not assignable to type 'T'.
      'Table' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'BaseTable'.(2417)

我的目标是拥有私有或受保护的构造函数,并能够分两步调用代码

const builder = Table.Builder.create();
const table = builder.build();

我认为这可能是由于Builder类是静态的,所以我尝试了这个:

export abstract class BaseTable {
  constructor() {}
}

export namespace BaseTable {
  export abstract class Builder<T extends BaseTable> {
    protected constructor() {}

    build(): T {
      return this.buildTable();
    }

    protected abstract buildTable(): T;
  }
}

export class Table extends BaseTable {
  constructor() {
    super();
  }
}

export namespace Table {
  export class Builder extends BaseTable.Builder<Table> {
    protected constructor() {
      super();
    }

    static create() {
      return new Table.Builder();
    }

    protected buildTable(): Table {
      return new Table();
    }
  }
}

此代码会产生相同的错误,并违背了拥有私有构造函数的目的。

有人可以向我解释这个错误以及如何实现这个代码。

标签: typescript

解决方案


问题是它BaseTable.Builder被声明为一个泛型类,可以作为T extends BaseTable消费者想要的任何东西。谁可以调用new BaseTable.Builder()(我不清楚那可能是谁,因为构造函数是protected))可以指定T,比如new BaseTable.Builder<{foo: string}>().

为了扩展BaseTable,类的静态端和实例端都需要分别分配给 的静态端和实例端BaseTable。即既有接口端继承,也有静态端继承;有关静态继承是否可取的一些讨论,请参见microsoft/TypeScript#4628 。然而,在可预见的未来,情况就是这样。

所以Table.Builder必须可以赋值给BaseTable.Builder。但事实并非如此。而Table.Builder可以(由谁?)用于创建调用者想要的build()任何类型的新事物,但只能(再次,由谁?)用于创建a .T extends BasetableBaseTable.Builderbuild()Table

如果Table正确 extends BaseTable,并且如果我们摆脱了隐私/保护修饰符,以便有人可以实际演示打字问题(并忽略构造签名),那么问题是:

const hmm = new BaseTable.Builder<{ foo: string }>().build().foo; // okay
const AlsoBaseTable: typeof BaseTable = Table;
const uhOh = new AlsoBaseTable.Builder<{ foo: string }>().build().foo;

您应该被允许在很大程度上将Table类构造函数视为具有与类构造函数相同的静态属性,如果您的实现不是通用的BaseTable,这将是一个问题。Table.BuilderBaseTable


所以,退一步说,我认为你并不想BaseTable.Builder成为泛型,或者至少不想像 TypeScript 的泛型所暗示的那样泛型。TypeScript 的泛型是通用的而不是存在的(有关更多信息,请参阅此 Q/A),所以当您说class <T extends BaseTable>{...}这适用于 . 的所有规范时,T而不仅仅是.T

相反,您可能想要做的是扩大 BaseTable.Builder,以便它只声称用 来做事情BaseTable,然后让子类在他们认为合适的时候缩小。这不一定强制执行您希望看到强制执行的所有约束,但只要您正确实现子类,事情应该会解决:

abstract class BaseTable {
    protected constructor() { }

    static Builder = class {
        protected constructor() { }

        build(): BaseTable {
            return this.buildTable();
        }

        protected buildTable(): BaseTable {
            throw new Error("Must be implemented in subclasses");
        }
    };
}


class Table extends BaseTable {
    private constructor() {
        super();
    }

    static Builder = class extends BaseTable.Builder {
        protected constructor() {
            super();
        }

        static create() {
            return new Table.Builder();
        }

        protected buildTable(): Table {
            return new Table();
        }
    };
}

现在没有错误(除了可能的声明警告和其他private/protected在夜间发生碰撞的事情),因为Table.Builder它是BaseTable.Builder.

Playground 代码链接


推荐阅读