使用 ConstructorParameters 扩展泛型 class?

Using ConstructorParameters to extend a generic class?

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

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.

的值之外,这个新的 subclass 构造函数应该采用 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 class 中对 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.

时一样

I don't know if there is an alternative strategy or feature of Typescript that can be leveraged, but since I'm short on time I'll just answer here why your approach doesn't work.

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

Child<G> 中声明的 G 类型参数仅约束代码中的两件事:

  1. 子 class 扩展的类型,即 Parent<G>

  2. 构造函数中 super 调用的预期参数。这是从#1 开始的。如果您将鼠标悬停在 IDE 中的 super 上,它将显示:

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

要进一步 confirm/understand 发生了什么,请将 class Parent<T> 更改为 class Parent<T extends number> 并查看错误如何变化。上面说的大家应该都清楚了吧

修复它的明显方法是使用 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 团队提交 Issue 以支持

ConstructorParameters<typeof Parent<G>>

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

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

祝你好运。

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

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>

这是我原来问题中的 classes 的作用:Playground Link

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



为什么有效

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

请注意,虽然我只见过 unknown 替换泛型作为对通用 class 调用 ConstructorParameters 的结果,但我不知道这是否得到保证每个场景。所以我希望其他人可以验证这一点。

如果这是正确的,那么接下来要做的就是可靠地确定什么是或不是 unknown。我创建了 IsUnknown 实用程序类型(为了便于阅读,在下面进行了扩展),在 IfAny 实用程序的帮助下,我从 获得了该实用程序。然而,这是另一个不能确定这在每一个场景中都有效的例子。如果确实如此,那么这应该意味着它是可扩展的,并且无论我的超级 class 使用什么其他类型,包括 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 实用程序(为了便于阅读,在下面进行了扩展)来完成大部分的腿部工作。由于 ,我想出了如何对可变元组进行递归,并且通过它我能够用我们的替换元组中下一个未使用的类型替换构造函数参数中的未知数。

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 类型将所有东西很好地包装在一起。

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

正在寻求反馈!