typescript 使用ConstructorParameters扩展泛型类?

9rygscc1  于 2023-06-30  发布在  TypeScript
关注(0)|答案(3)|浏览(158)

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

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一样。

ntjbwcob

ntjbwcob1#

我不知道是否有一个替代的策略或Typescript的特性可以利用,但由于我时间有限,我只会在这里回答为什么你的方法不起作用。
在您的代码中,ConstructorParameters<typeof Parent>中的项Parent不受G类型参数的约束。因此它的隐式类型是Parent<unknown>
Child<G>中声明的G类型参数只约束代码中的两件事:
1.子类扩展的类型,即Parent<G>
1.在ctor中对super调用的预期args。这是从#1开始。如果您将鼠标悬停在IDE中的super上,它将显示:

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

要进一步确认/理解发生了什么,请将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团队提交问题以获得支持

ConstructorParameters<typeof Parent<G>>

你可能会得到一个“好主意!”的回答,或者你的问题的解决方案(指向这个SO问题)。如果您确实提交了一个问题,请在您的问题中发布一个链接。
希望比我聪明的人能看到这一点,并提出解决方案。
祝你好运

cunj1qz1

cunj1qz12#

在TS版本4.3.5中创建了我自己的实用程序类型来解决这个问题。在处理泛型时,我使用它来代替常规的ConstructorParameters,它看起来具有很好的可扩展性,不过如果您想确定的话,请务必阅读解释部分。下面是一个带有helper的文件,沿着几个示例playgrounds:

泛型构造函数参数.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链接
这里是如果Parent类接受多个泛型:Playground链接

为什么会这样

GenericConstructorParameters有两个参数。它的工作原理是在第一个参数(构造函数,就像ConstructorParameters的第一个参数)上调用ConstructorParameters,以获取构造函数参数类型的元组。因为泛型在被ConstructorParameters提取时变成了unknown,所以我们用第二个参数提供的类型(替换类型的元组)替换结果元组中的每个unknown类型。
请注意,虽然我只见过unknown在泛型类上调用ConstructorParameters时替换泛型,但我不知道是否在 every 场景中都能保证这一点。所以我希望其他人能证明。
如果这是正确的,那么可靠地确定什么是或不是真正的unknown是下一步要做的事情。我创建了IsUnknown实用程序类型(为了便于阅读,在下面进行了扩展)来实现这一点,并得到了来自this SO answerIfAny实用程序的帮助。然而,这是另一个不确定这在每一个场景中都有效的例子。如果是这样的话,那么这应该意味着它是可伸缩的,并且无论我的超类使用什么其他类型,包括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类型只是将这些东西很好地 Package 在一起。
我不知道这个用例有多小众,或者是否有人以前解决过这个问题,但我希望这是(实际上是正确的),能够帮助其他人解决同样的问题。
寻找反馈!

ffx8fchx

ffx8fchx3#

这也可以工作,并且更易于管理

type GenericConstructorParameters<T> = ConstructorParameters<new ( ...args: any[] ) => T>

相关问题