【问题标题】:Using ConstructorParameters to extend a generic class?使用 ConstructorParameters 扩展泛型类?
【发布时间】:2021-07-16 21:44:59
【问题描述】:

假设我有以下泛型类,它有两个字段:一个是字符串,另一个是完全任意的泛型——无论我希望它在实例化时是什么:

class Parent<T> {
    stringField: String
    genericField: T;

    constructor(stringField: String, genericField: T) {
        this.stringField = stringField;
        this.genericField = genericField;
    }
}

实例化它可以按预期工作,例如new Parent("hello", "world").genericField 为字符串提供智能感知,new Parent("hello", 10000).genericField 为数字提供智能感知等。

现在假设我想扩展Parent。除了stringFieldgenericField 的值之外,这个新的子类构造函数还应采用Parent 构造函数中不存在的一个附加参数。

但是,我不想只是复制粘贴 Parent 参数,因为如果我需要更改 Parent,那是不可扩展的。所以相反,我想使用ConstructorParameters 实用程序类型来推断那些冗余参数并将它们自动传递给super 调用,类似于这样:

class Child<G> extends Parent<G> {
    numberField: Number;

    constructor(numberField: Number, ...params:ConstructorParameters<typeof Parent>) {
        super(...params);
        this.numberField = numberField;
    }
}

但是,这并没有按预期工作。上面在Child 类中对super 的调用会产生以下编译器错误:Argument of type 'unknown' is not assignable to parameter of type 'G'. 'G' could be instantiated with an arbitrary type which could be unrelated to 'unknown'.

确实,写new Child(22, "hello", "world").genericField 确实 为字符串提供智能感知,因为genericField 在这里始终是unknown 类型,当我希望它是我传递给它的任何类型时,就像我实例化Parent时一样。

【问题讨论】:

  • 请提供一个完整的例子——即实际实例化 Child 并显示您的期望。你的命名也很混乱。为什么otherStaticTypestaticType genType 命名为____type
  • @Inigo 感谢您让我知道我的问题不清楚。我只是用实例化和更好的命名字段对其进行了重大编辑,希望能提供更多的清晰度。
  • @Inigo,我也尝试了很多东西,但都没有奏效。如果您可以考虑改变方法,我想谈谈,但这需要了解确切的业务案例
  • @OleksandrKovalenko 感谢您对此进行调查。我刚刚发布了我自己的答案,这似乎有效,但由于我对某些 TS 类型的一些不确定性,我不能 100% 确定。如果你有时间,请告诉我我的假设是否成立!

标签: typescript generics intellisense


【解决方案1】:

我不知道是否可以利用 Typescript 的替代策略或功能,但由于我时间紧迫,我将在这里回答为什么您的方法不起作用。

在您的代码中,ConstructorParameters&lt;typeof Parent&gt; 中的术语 Parent 不受 G 类型参数的约束。因此它的隐式类型是Parent&lt;unknown&gt;

Child&lt;G&gt; 中声明的G 类型参数仅在您的代码中限制了两件事:

  1. 子类扩展的类型,即Parent&lt;G&gt;

  2. ctor 中 super 调用的预期参数。这来自#1。如果您在 IDE 中将鼠标悬停在 super 上,它将显示:

    constructor Parent<G>(stringField: String, genericField: G): Parent<G>
    

要进一步确认/了解发生了什么,请将class Parent&lt;T&gt; 更改为class Parent&lt;T extends number&gt; 并查看错误如何变化。我上面说的现在应该很清楚了。

修复它的明显方法是使用G 类型参数来约束ConstructorParameters,例如:

class Child<G> extends Parent<G> {
    numberField: Number;

    constructor(numberField: Number, ...params:ConstructorParameters<typeof Parent<G>>) {
        super(...params);
        this.numberField = numberField;
    }
}

但是 Typescript 不支持这种语法,甚至可能不支持语义。

也许有一种方法可以使用infer 或定义一个与Parent 绑定的自定义ConstructorParameters,但我现在没有时间玩这个。

这是一个有趣的问题。我认为 TS 会有一个解决方案,或者想要一个解决方案。我会向 TS 团队提交一个问题来支持

ConstructorParameters<typeof Parent<G>>

您可能会得到一个“好主意!”响应,或解决您的问题(指向此 SO 问题)。如果您确实提交了问题,请在您的问题中发布指向它的链接。

希望比我更聪明的人会看到这一点并提出解决方案。

祝你好运。

【讨论】:

  • 非常感谢您的回答。这对我重新思考这个问题很有帮助,在玩了这个问题之后(太久了),我想我想出了一个我刚刚在下面发布的解决方案。对于我的一些假设是否成立、这可能导致的任何问题等,我肯定会很感激。
【解决方案2】:

在 TS 版本 4.3.5 中创建了我自己的实用程序类型来解决这个问题。在处理泛型时,我使用它来代替常规的 ConstructorParameters,它看起来非常具有可扩展性,但如果你想确定的话,请务必阅读解释部分。这是一个带有助手的文件,以及几个示例 Playground:

GenericConstructorParameters.ts

// helpers
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N; 
type IsUnknown<T> = unknown extends T ? IfAny<T, never, true> : never;
type UnknownReplacer<T, K> = K extends [infer WithThis, ...infer WithRest] ? T extends [infer ReplaceThis, ...infer ReplaceRest] ? IsUnknown<ReplaceThis> extends never ? [ReplaceThis, ...UnknownReplacer<ReplaceRest, K>] : [WithThis, ...UnknownReplacer<ReplaceRest, WithRest>] : []: T


// GenericConstructorParameters: Takes two arguments
// Arg 1. a constructor
// Arg 2. a tuple of types (one type for each generic in the constructor)
type GenericConstructorParameters<T extends abstract new (...args: any) => any, K> = UnknownReplacer<ConstructorParameters<T>, K>

这与我原来问题中的课程一起行动:Playground Link

如果Parent 类采用多个泛型:Playground Link



为什么有效

GenericConstructorParameters 接受两个参数。它通过在其第一个参数(构造函数,就像ConstructorParameters 的第一个参数)上调用ConstructorParameters 来获取构造函数参数类型的元组。因为泛型在由ConstructorParameters 提取时变为unknown,因此我们将结果元组中的每个unknown 类型替换为我们的第二个参数提供的类型(替换类型的元组)。

请注意,虽然我只见过unknown 在泛型类上调用ConstructorParameters 来替换泛型,但我不知道这是否在每个 场景中都能得到保证。所以我希望其他人可以验证这一点。

如果这是正确的,那么可靠地确定什么是或不是真正的 unknown 是接下来要做的事情。在IfAny 实用程序的帮助下,我创建了IsUnknown 实用程序类型(为了便于阅读而在下面进行了扩展),该实用程序是从this SO answer 获得的。但是,这是另一个不确定这在每种情况下都适用的情况。如果确实如此,那么这应该意味着它是可扩展的,并且无论我的超类使用什么其他类型(包括 any 类型)都应该工作。

type IsUnknown<T> =
  unknown extends T         // unknown only extends either itself or any (I think)
    ? IfAny<T, never, true> // so if we can narrow it down, just check if it is any
    : never;

我创建了UnknownReplacer 实用程序(为了便于阅读,在下面进行了扩展)来完成大部分工作。感谢this SO answer,我想出了如何对可变参数元组进行递归,并且通过它,我可以用我们替换元组中下一个未使用的类型替换构造函数参数中的未知数。

type UnknownReplacer<T, K> =
  K extends [infer WithThis, ...infer WithRest]                   // if K is a populated tuple of types (i.e. one for each unknown to replace)
    ? T extends [infer ReplaceThis, ...infer ReplaceRest]         // ...then if T is a populated tuple of types (i.e. from our constructor arguments)
      ? IsUnknown<ReplaceThis> extends never                      // ......then check if the first type in T is NOT unknown
        ? [ReplaceThis, ...UnknownReplacer<ReplaceRest, K>]       // .........and if not unknown, return the first type from T with a recursive call on the remaining args from T
        : [WithThis, ...UnknownReplacer<ReplaceRest, WithRest>]   // .........but if it is unknown, return the first type from K with a recursive call on the remaining args from T and K 
      : []                                                        // ......but if T is empty (or invalid), return an empty tuple as we've run out of things to check
    : T                                                           // ...but if K is empty (or invalid), return T as there's nothing we can use to replace anyway

最后,GenericConstructorParameters 类型只是很好地将所有东西包装在一起。

我不知道这个用例有多小众,或者是否有人以前解决过这个问题,但我希望这(实际上是正确的并且)能够帮助遇到同样问题的其他人。

寻求反馈!

【讨论】:

  • 我相信你的解决方案没问题。你有我的赞成票
  • 我很高兴你找到了有用的东西!我现在没有时间消化你做了什么,但粗略地看它很复杂!在我看来,您最初的需求并不是那么深奥,应该更容易得到 Typescript 的支持。你能提交一个关于 Typescript 的问题吗?指向这个 SO 问题和您的答案(如果有帮助,还有我的答案)?
  • @Inigo 答案很长,因为我解释了为什么它可能有效,但实际的解决方案只是另一种实用程序类型,尽管我想让它和解释之间的区别更清楚。但是现在我可以使用我自己的ConstructorParametersGeneric 类型来代替ConstructorParameters。而且由于我想我能够使用适当的 TS 创建它,我不确定这是否值得一个完整的 TS 问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-04-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-02-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多