Typescript 类在为任何子类键入的基类中使用静态方法

问题描述 投票:0回答:1

我正在尝试为 Web 组件编写一个包装器,它允许您通过引用它们的密钥在 JSX 中使用它们;如果尚未注册自定义元素,这也会隐式注册,这样您就不会忘记注册。

是否可以有一些定义静态方法的抽象基类,并有扩展此类的子类,并在访问该方法时具有正确的类型。

例如:

abstract class Base {
  static _key: string

  static get key() {
    console.log(this._key)

    return this._key
  }
}

class Child extends Base {
  static _key = "child" as const
}

// Want key to be of type "child", not string
const key = Child.key

如果可能的话,我更愿意做这样的事情(因为这将允许我输入道具而不必到处修改

IntrinsicElements
),但我注意到类似的问题

abstract class Base {
    abstract props: Record<string, unknown>

    static Render<T extends typeof Base>(this: T, props: InstanceType<T>["props"]) {
        return "jsx-representation-of-element"
    }
}

class Child extends Base {
    props = { hello: "world" }
}

// Some other file, test.tsx
// Expecting that the type is inferred to need Child's props, not Base's
// It doesn't care that hello is missing because the typeof this is
// is inferred to be Base
Child.Render({ hello: "yb" }) // okay 👍
{ <Child.Render hello="yb" /> } // okay 👍

Child.Render({ hello: 1 }) // error 👍
{ <Child.Render hello={1} /> } // NO ERROR?! 😢

我已经能够通过将

static key: typeof this._key
添加到
Child
来解决这个问题,但这需要我在任何地方添加它,所以希望我能找到一个不需要这个的解决方案。

对于第一部分,我研究了使用

this
作为参数,但这不能在吸气剂上完成。我意识到我可以通过使其成为一个普通方法来解决这个问题,然后在组件函数之外调用它一次,在 jsx 标记内引用该变量,但这似乎相当冗长。 我还尝试让
Base
采用泛型参数并在
Base.key
中返回它,忽略有关静态成员和泛型类型的错误,但这只是使类型
T
(实际上是
any

对于第二部分,我可以使用

this
参数,但如上所述,
Child.Render
函数认为
this
类型是
Base
,因为它尚未被调用。

显然,一旦我调用

Child.Render()
,类型就可以了,但如果在 jsx 标签内使用,这将不起作用。

我可以使用一些 tsconfig 选项将其默认为

Child
this
吗?

javascript typescript class generics jsx
1个回答
0
投票

正如您所注意到的,TypeScript 不支持

this
类成员多态
static
类型
microsoft/TypeScript#5863 上有一个长期悬而未决的问题,要求提供此类功能,但它目前不是该语言的一部分。看起来您尝试了在 generic
 静态方法上使用 
this 参数 的“标准”解决方法。

当您直接调用该方法时,这是可行的,但正如您所看到的,这对于 JSX 元素来说,不适用于。这是 TypeScript 中的一个已知错误,其中函数中的

this
参数未使用 JSX 元素内的类型参数正确实例化。此问题已在 microsoft/TypeScript#55431 中报告,目前仍未修复。

所以你必须放弃或解决它。目前我能想到的唯一解决方法是更改泛型类型参数的范围,以便该方法已经知道它,并且可以直接引用而无需

this
参数。这意味着您希望
Base
是通用的......但这并不完全有效,因为静态成员无法访问类型参数。相反,您可以将
Base
制作为 class factory 函数,以 returns 类构造函数。 像这样:

function Base<T extends Record<string, unknown>>(initProps: T) {
    return class {
        props: T = initProps

        static Render(props: T) {
            return "jsx-representation-of-element"
        }
    }
}

所以这里

Base
本身并不是一个类。相反,您必须 call
Base
并传递初始的
props
。然后它重新调整一个类,该类已知
T
的类型
props

class Child extends Base({ hello: "world" }) { }

所以

Base({hello: "world"})
返回的类是不是通用的。类型
{hello: string}
只是该类类型的一部分,因此
Base({hello: "world"}).Render()
只接受类型为
{hello: string}
的参数。 因此
Child
也是如此,因为它是
Base({hello: "world"})
的(当前空白)扩展。

这会产生您正在寻找的行为:

Child.Render({ hello: "yb" }) // okay 👍
{ <Child.Render hello="yb" /> } // okay 👍

Child.Render({ hello: 1 }) // error 👍
{ <Child.Render hello={1} /> } // error 👍

这回答了所提出的问题。但有一个警告:类工厂函数与超类不同。

请注意,类工厂在运行时并不真正使用继承,因此您不能轻松地使用(例如)

instanceof
运算符来检查某些内容是否是
Base
的后代。当然
child instanceof Base
将会是 false,因为
Base
不是一个类,并且
child instanceof Base({hello: "yb"})
也会是 false。但由于每次调用
Base()
都会生成一个新类,因此两个不同的
Base()
输出没有共同的父类。 您也许可以尝试一下并让它发挥作用,但这并不简单。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.