有没有办法使用 JSDoc 定义这样的函数的返回类型,它创建一个扩展用户提供的类作为函数参数的类?
/**
* @param {class} UserProvidedClass
* @returns ???
*/
function createClientClass(UserProvidedClass) {
return class ClientClass extends UserProvidedClass {
bindServerSocket(socket) {
this.server = socket;
this.onConnect();
}
onConnect() {
super.onConnect?.();
if (DEBUG) console.log(`client connected`);
}
...
};
}
因为这可以工作,但如果 UserProvidedClass 不“覆盖”它们,当然不会包含任何动态类的方法和字段:
/**
* @param {class} UserProvidedClass
* @returns {UserProvidedClass}
*/
function createClientClass(UserProvidedClass) {
return class ClientClass extends UserProvidedClass {
bindServerSocket(socket) {
this.server = socket;
this.onConnect();
}
onConnect() {
super.onConnect?.();
if (DEBUG) console.log(`client connected`);
}
...
};
}
替代方案似乎“被破坏”:如果我们将动态类作为真正的类取出,那么我们就会遇到继承问题:
class BaseClass {
bindServerSocket(socket) {
this.server = socket;
this.onConnect();
}
onConnect() {
super.onConnect?.();
if (DEBUG) console.log(`client connected`);
}
...
}
/**
* @param {class} UserProvidedClass
* @returns {T extends BaseClass, UserProvidedClass} JS cannot do this
*/
function createClientClass(UserProvidedClass) {
return class ClientClass extends ...something JS can't do {
...
};
}
这是 JSDoc 无法做到的事情之一,还是有一种 JSDoc 方法可以完成 TS 对类型和接口所做的事情,而无需编写“无操作”JS 来捕获某些类的方法和属性,而无需编写除了类型文档之外,你还用过其他东西吗?
按照建议,我阅读了关于 generics 和 templates 的文档,但要么我跳过了某些内容,要么给出的示例未涵盖这一点,因为使用模板我能想出的所有内容都会产生返回类型类型提示是:
/**
* @template T
* @template { bindServerSocket(socket:WebSocket):void,onConnect():void } U
* @param {T} UserProvidedClass
* @returns {V implements U extends T}
*/
function createClientClass(UserProvidedClass) {
return class ClientClass extends UserProvidedClass {
bindServerSocket(socket) {
this.server = socket;
this.onConnect();
}
onConnect() {
super.onConnect?.();
console.log(`connected`);
}
};
}
const ClientClass = createClientClass(
class {
test() {
console.log(`test`);
}
}
);
const client = new ClientClass();
client.bindServerSocket(new WebSocket(`http://localhost`));
client.test();
函数的返回类型现在标记为
implements U extends T
,ClientClass
的键入提示为 ClientClass
,虽然 bindServerSocket
解析为动态类方法,但 test
不解析为传递给函数的类。
(同样的情况也发生在
@return {U extends T}
上,其中输入可以解析来自 U 的函数,但不能解析来自 T 的函数)
一种方法是使用
@template
注解来指定泛型类型参数 T
,它表示调用者提供的基类。然后,您可以使用 @extends
关键字后跟模板参数名称 (T
) 来指示返回的类应扩展指定的基类。最后,您可以使用 @returns
注释来指定函数的返回类型,这将是扩展基类的新创建的类。这是一个例子:
/**
* Creates a new class that extends the provided base class.
*
* @template T - The base class to extend.
* @returns A new class that extends {@linkcode T}.
*/
function createClientClass(baseClass) {
// Implementation details omitted...
}
在此示例中,
@template
注释指定该函数采用名为 T
的单个类型参数。该类型参数表示调用者想要扩展的基类。 @returns
注解表示函数返回类型为A
的值,其中A
定义如下:
type A<T> = {
new(...args: any[]): T & {
bindServerSocket(socket: WebSocket): void;
onConnect(): void;
};
};
此类型定义使用 TypeScript 语法来表示构造函数,该函数接受任意数量的参数并返回扩展类的实例。
&
运算符用于将基类的类型与扩展添加的附加成员相交。在这种情况下,附加成员是 bindServerSocket()
和 onConnect()
。
要在代码中使用此类型定义,您需要在项目中启用 TypeScript 支持。如果您使用的是 Visual Studio Code,则可以将以下行添加到您的
tsconfig.json
文件中:
{
"compilerOptions": {
"target": "es6",
"moduleResolution": "node",
"strict": true,
"jsx": "react"
},
"include": ["src"]
}
启用 TypeScript 支持后,您可以导入
createClientClass()
函数并使用基类作为其参数来调用它:
import { createClientClass } from './createClientClass';
// Create a new class that extends the 'BaseClass' class.
const ExtendedClass = createClientClass(BaseClass);
// Instantiate the extended class.
const instance = new ExtendedClass('foo', 'bar');
// Call the 'bindServerSocket()' method on the instance.
instance.bindServerSocket(new WebSocket('ws://example.com'));