为什么通用约束会产生 TS2417 错误?

时间:2020-12-21 20:13:58

标签: typescript

我在实现以下代码的 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();
    }
  }
}

这段代码产生了同样的错误并且违背了拥有私有构造函数的目的。

谁能向我解释这个错误以及如何实现这段代码。

1 个答案:

答案 0 :(得分:1)

问题在于 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() 调用者想要的任何类型T extends Basetable 的新事物,BaseTable.Builder 只能用于(同样,由谁?) 创造build() Table 的新事物。

如果 Table 正确扩展 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.Builder 实现,这将是一个问题与 BaseTable 的通用方式不同。


所以,退一步来说,我认为您并不真的希望 BaseTable.Builder 成为泛型,或者至少不像 TypeScript 的泛型所暗示的那样泛型。 TypeScript 的泛型通用而不是存在(见this Q/A for more info),所以当你说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 是 {{1} 的真正子类型}.

Playground link to code