提问人:Mike 'Pomax' Kamermans 提问时间:11/5/2023 最后编辑:Mike 'Pomax' Kamermans 更新时间:11/18/2023 访问量:239
为创建动态类的函数添加 JSDoc 返回类型
Adding a JSDoc return type for a function that creates a dynamic class
问:
有没有办法将 JSDoc 与纯 JavaScript(不是 TypeScript)一起使用来定义此类函数的返回类型,该函数会创建一个新类作为输出,该类是作为函数输入提供的用户提供的类的扩展?
/**
* Build a new class that extends the UserProvidedClass, and return that.
* @param {class} UserProvidedClass
* @returns ???
*/
function createClientClass(UserProvidedClass) {
return class ClientClass extends UserProvidedClass {
// this class will not have an explicit constructor.
bindServerSocket(socket) {
this.server = socket;
this.onConnect();
}
onConnect() {
super.onConnect?.();
if (DEBUG) console.log(`client connected`);
}
};
}
// And then an example of how this gets used:
class CustomClass {
// this class will also not have an explicit constructor.
testServer() {
console.log(`instance has server:`, !!this.server);
}
}
const ClientClass = createClientClass(CustomClass);
// Typing-aware editors and tooling should now know that
// this instance has three methods on it:
const instance = new ClientClass();
// And things like VS Code should be able to see that the following
// function is from the class inside "createClientClass":
instance.bindServerSocket(new WebSocket(`ws://localhost:800`));
// And that the following function is from "CustomClass":
instance.testServer();
以下内容在技术上“有效”,因为严格来说,键入并不错误,但当然不会包含任何不在 UserProvidedClass 中的动态类的方法和字段:
/**
* We are strictly speaking returning an instance of UserProvidedClass
* here, but by typing it as such, all the methods added by the extension
* will be lost.
*
* @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`);
}
...
};
}
// instance type hints will not show `bindServerSocket` or `onConnect`
另一种选择似乎更有问题:如果我们把动态类作为一个真正的类来拉出来,那么我们就会遇到继承问题,因为JS不允许对类进行多次继承:
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 ??? {
// We can't extend both the BaseClass and the UserProvidedClass.
};
}
这是 JSDoc 无法做到的事情之一,还是有一种 JSDoc 方法可以完成 TS 对类型和接口所做的事情,而不必编写“no-op”JS 来捕获某个类的方法和属性,而不会用于类型文档以外的任何用途?
正如建议的那样,我阅读了有关泛型和模板的文档,但是我要么跳过了某些内容,要么给出的示例没有涵盖这一点,因为对于模板,我所能想到的在类型提示中生成返回类型的是:
/**
* @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 instance = new ClientClass();
instance.bindServerSocket(new WebSocket(`http://localhost`));
instance.test();
其中,函数的返回类型现在被标记为 ,并且虽然解析为动态类方法,但不解析为传递到函数的类的方法。implements U extends T
ClientClass
ClientClass
bindServerSocket
test
(同样的情况也发生在 中,其中类型可以从 U 解析函数,但不能从 T 解析函数)@return {U extends T}
答:
/**
* Creates a new class that extends the given base class. The new class
* will have additional methods 'bindServerSocket' and 'onConnect'.
*
* @template T - The base class to extend.
* @param {new(...args: any[]) => T} UserProvidedClass - The class to be extended.
* @returns {new(...args: any[]) => T & ClientClassMethods} - A new class that extends the UserProvidedClass and adds new methods.
*/
function createClientClass(UserProvidedClass) {
return class ClientClass extends UserProvidedClass {
bindServerSocket(socket) {
this.server = socket;
this.onConnect();
}
onConnect() {
super.onConnect?.();
if (DEBUG) console.log(`client connected`);
}
};
}
/**
* This is a helper type that defines additional methods added by ClientClass.
* @typedef {Object} ClientClassMethods
* @property {Function} bindServerSocket - Method to bind a server socket.
* @property {Function} onConnect - Method called on connection.
*/
// Usage Example:
const instance = createClientClass(CustomClass);
instance.bindServerSocket(new WebSocket('ws://localhost:8000'));
instance.testServer();
评论
@extends
keyword followed by the name of the template parameter (T
) to indicate that the returned class should extend the specified base class"? Because that's the core of the question, so right now this post isn't an answer yet, it's still missing part of the JSDoc for this function. Also note this is explicitly not TypeScript: this is plain JS with JSDoc comments, so there is no tsconfig, there's no or keyword, etc. It's just pure JS, and typing through comments.type
interface
@returns
has to include the type information. If it's not , it's not a JSDoct return type hint; the format you're using in your answer does not include the actual typing the question asks for)@returns {returnTypehere} and then the description
This appears to not officially be possible due to there not being any official JSDoc syntax for indicating that a parameter or return is for "an actual class definition" rather than for "an instance of some class".
There are a number of issues about this on the JSDoc issue tracker, with the most appropriate appearing to be issue 1349, "Document class types/constructor types", filed in 2017, which has not been resolved as of the time of this post in late 2023.
See Julio's answer for a possible approach, but note that there do not appear to be any official or even unofficial docs for the typing tricks used, and so that may or may not work for you depending on when you find this post, or the tooling you're using.
(VS Code with the source marked as TypeScript to maximize type-awareness, for instance, can tell that is defined in the function, but cannot tell that is defined in instance.bindServerSocket()
createClientClass
instance.test()
CustomClass
)
The return type can only be properly defined if , or a corresponding (the equivalent of an interface), is not hidden inside .ClientClass
typedef
createClientClass
Below is some code, simply using a , that seems to do the trick (tested at playcode.io). Also, you had a error in your code: returns a constructor, not an instance.typedef
createClientClass
By the way, I find your construction a bit "suspect", but I don't know your actual design requirements, and I have only replied to your direct question.
/**
* Represents the 'ClientClass' component of
* the return type of {@link createClientClass}.
*
* @typedef IClientClass
* @type {object}
* @property {(socket: string) => void} bindServerSocket
*/
/**
* Returns a class of type {@link IClientClass} that
* extends the given {@link UserProvidedClass}.
*
* @template {class} T
* @param {abstract new () => T} UserProvidedClass
* @returns {new () => IClientClass & T}
*/
function createClientClass(UserProvidedClass) {
return class ClientClass extends UserProvidedClass {
bindServerSocket(socket) {
this.server = socket;
}
};
}
// And then an example of how this gets used:
class CustomClass {
testServer() {
console.log(`instance has server:`, !!this.server);
}
}
// create the constructor
const clientClass = createClientClass(CustomClass);
// create an instance
const clientInstance = new clientClass();
// check methods are visible
clientInstance.bindServerSocket("1");
clientInstance.testServer();
A side note about "reputable sources": it is not even possible here to link to the reference documentation since 1) the JSDoc reference documentation is minimal to say the least and in fact not at all exhaustive; moreover, 2) most tooling actually leverages TypeScript for parsing JSDoc typing annotations and checking code, so results are not strictly determined by JSDoc anyway, results here are more of a half way between what JSDoc is supposed to offer and the support that TS engines actually give to typing annotations. And none of that is properly documented anywhere.
评论
UserClientClass
class{}
@template