【问题标题】:Enforce 'typeof BaseClass' when base class is generic当基类是泛型时强制执行“typeof BaseClass”
【发布时间】:2021-07-03 02:52:20
【问题描述】:

假设我有一个通用抽象基类和一组扩展基类并提供通用参数的类。

abstract class BaseClass<T> {
  constructor(protected container: T[]) { }
  static someStaticFunc(): string {
    return 'I\'m the BaseClass';
  }
  getElement(i: number): T {
    return this.container[i]
  }
}

class ConcreteClass1 extends BaseClass<number> {
  static someStaticFunc(): string {
    return 'I\'m ConcreteClass1'
  }

  addNumber(element: number) {
    this.container.push(element);
  }
}

class ConcreteClass2 extends BaseClass<string> {
  static someStaticFunc(): string {
    return 'I\'m ConcreteClass2'
  }

  addStringTwice(element: string) {
    this.container.push(element);
    this.container.push(element);
  }
}

class ConcreteClass3 extends BaseClass<boolean> {
    // Doesn't add anything
}

我定义了一个接口结构,其中每个值都必须是typeof BaseClass

interface SetOfBaseClasses {
  Class1: typeof BaseClass,
  Class2: typeof BaseClass,
  Class3: typeof BaseClass
};

我想创建一个该接口类型的对象,并将具体的类分配给它。然后调用类上的每一个静态函数,在底部得到预期的结果。

const classes: SetOfBaseClasses = {
  Class1: ConcreteClass1,
  Class2: ConcreteClass2,
  Class3: ConcreteClass3
}

console.log(classes.Class1.someStaticFunc());
console.log(classes.Class2.someStaticFunc());
console.log(classes.Class3.someStaticFunc());

// Expected output:
// I'm ConcreteClass1
// I'm ConcreteClass2
// I'm the BaseClass

但是,TypeScript 不认为 ConcreteClass1ConcreteClass2ConcreteClass3typeof BaseClass 兼容。我认为这是因为 BaseClass 涉及的泛型。所以我也尝试了:

interface SetOfBaseClasses {
  Class1: typeof BaseClass<unknown>,
  Class2: typeof BaseClass<unknown>,
  Class3: typeof BaseClass<unknown>
};

更准确地说:

interface SetOfBaseClasses {
  Class1: typeof BaseClass<number>,
  Class2: typeof BaseClass<string>,
  Class3: typeof BaseClass<boolean>
};

在任何一种情况下,TypeScript 似乎都不允许在 typeof Class 之后使用泛型参数

界面中的每一行都有两个错误,如下所示:

Call signature, which lacks return-type annotation, implicitly has an 'any' return type.(7020)
Type parameter name cannot be 'number'.(2368)

有没有办法用 TypeScript 做这种事情?在涉及泛型之前,这种方法似乎一切正常。

Typescript Playground

【问题讨论】:

    标签: typescript typescript-generics


    【解决方案1】:

    typeof BaseClass 类型或多或少等价于这个:

    type TypeofBaseClass =
      (abstract new <T>(container: T[]) => BaseClass<T>) &
      { someStaticFunc(): string };
    
    const testTypeofBaseClass: TypeofBaseClass = BaseClass;
    

    注意TypeofBaseClass本身不是generic(上面没有类型参数);相反,它有一个通用的abstract construct signature。这很像一个泛型函数,因为泛型类型参数是在您调用时指定的,而不是在声明本身中。 typeof BaseClass 的值(或者更确切地说,它的具体通用子类,如 class ConcreteSubclass&lt;T&gt; extends BaseClass&lt;T&gt;)可以为编写 new ConcreteSubclass 的人想要的任何 T 构造一个 BaseClass&lt;T&gt;。它可以是 BaseClass&lt;string&gt;BaseClass&lt;number&gt; 或其他任何东西。

    那不是你想要的类型。


    相反,你想要的类型更像是这样的:

    type TypeofConcreteClass<T> =
      (new (container: T[]) => BaseClass<T>) &
      { someStaticFunc(): string; };
    

    这是一个具体的构造函数,所以 abstract 被删除了。但更重要的是,泛型类型参数T 的范围已被移动。您希望能够指定 TypeofConcreteClass&lt;number&gt; ,它只能构造 BaseClass&lt;number&gt; 实例而不是其他任何东西。写new ConcreteClass1 的人将永远无法得到BaseClass&lt;string&gt;


    目前无法自动将typeof BaseClass 转换为TypeofConcreteClass&lt;T&gt; 而不会跳过丑陋的圈子。该语言实际上并没有办法以编程方式表示它们之间的关系。有关更多信息,请参阅 "TypeScript how to create a generic type alias for a generic function?" 及其答案。

    对于上述情况,我建议只使用手动编写的TypeofConcreteClass&lt;T&gt; 定义。或者你可以通过做一些类似的事情来实现这一目标

    type TypeofConcreteClass<T> =
      Pick<typeof BaseClass, keyof typeof BaseClass> &
      (new (container: T[]) => BaseClass<T>);
    

    它以编程方式获取BaseClass 的所有static 属性/方法,同时手动写出构造签名。


    让我们验证一下这是否符合您的要求:

    interface SetOfBaseClasses {
      Class1: TypeofConcreteClass<number>,
      Class2: TypeofConcreteClass<string>,
      Class3: TypeofConcreteClass<boolean>
    };
    
    function main() {
      const classes: SetOfBaseClasses = {
        Class1: ConcreteClass1,
        Class2: ConcreteClass2,
        Class3: ConcreteClass3
      }
    
      console.log(classes.Class1.someStaticFunc());
      console.log(classes.Class2.someStaticFunc());
      console.log(classes.Class3.someStaticFunc());
    
      console.log(new classes.Class1([1, 2, 3]).getElement(2).toFixed(3)) // "3.000"
      console.log(new classes.Class2(["a", "b", "c"]).getElement(2).toUpperCase()) // "C"
    
    }
    

    编译没有错误,可以按需要工作。

    Playground link to code

    【讨论】:

    • 谢谢!这几乎正​​是我想要做的。另一件事可能是我如何最好地表示SetOfBaseClasses,我不关心作为通用参数传入的类型(至少在这部分代码中)。在这种情况下,我习惯于使用 unknown 作为通用参数,但它在这里不起作用。 any 似乎有效,但我尽量避免这种情况。也许在这里这不是一个真正的问题。
    • TypeScript 不直接支持存在量化的泛型类型(请参阅ms/TS#14466),因此没有简单的类型安全方式可以说“TypeofConcreteClass&lt;T&gt; for 一些 T我不在乎”。为了简单起见,您可以使用any 并失去一些类型安全性,或者您可以使用辅助函数来推断T,这样您就不必手动指定它。但是,存在往往是不必要的。 “我不知道其参数的具体子类”对您有什么好处?我会继续,但这超出了评论链的范围。
    • 知道了。在这种情况下,我正在构建一个框架,它只关心那些静态方法的存在和返回字符串是否存在于 Class 类型上。 T 在这种情况下可能是无限任意的,因此为所有可能的 T 推断 T 是不可能的。泛型中指定的类型更多是 Class 实现的内部关注点,并指定组件期望从外部数据源接收的数据类型。从我的人为/晦涩的例子中显然根本不清楚。但是感谢您的详细解释和建议!
    • 哦,那么也许您只需要{ someStaticFunc(): string; }(或等效项)而不用担心构造函数/泛型部分。任何具有正确方法的对象都将被允许,包括所有这些子类构造函数。祝你好运!
    猜你喜欢
    • 2019-07-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-22
    • 2010-09-28
    • 2021-10-01
    • 2021-07-28
    相关资源
    最近更新 更多