为创建动态类的函数添加 JSDoc 返回类型

Adding a JSDoc return type for a function that creates a dynamic class

提问人:Mike 'Pomax' Kamermans 提问时间:11/5/2023 最后编辑:Mike 'Pomax' Kamermans 更新时间:11/18/2023 访问量:239

问:

有没有办法将 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 TClientClassClientClassbindServerSockettest

(同样的情况也发生在 中,其中类型可以从 U 解析函数,但不能从 T 解析函数)@return {U extends T}

JavaScript JSdoc

评论

0赞 jonrsharpe 11/5/2023
看来你想做这个通用的,typescriptlang.org/docs/handbook/......。然后,返回类型是输入和其他方法的并集。
0赞 Mike 'Pomax' Kamermans 11/5/2023
不过,这不会捕获动态扩展本身的方法和属性,不是吗?这只会让它返回“通话期间的任何东西”,而不是工会。UserClientClass
0赞 jonrsharpe 11/5/2023
这将取决于你如何使用它,如果你有一个更具体的问题,请给出一个最小的可重复的例子
0赞 Mike 'Pomax' Kamermans 11/5/2023
这是确切的例子。该函数传递一个类(可以是任何内容,包括 just ),它返回一个新类,该类是该传入类的扩展,具有更多方法和属性。问题是如何编写 JSDoc 将返回类型标记为具有两者的所有属性和方法的类(类型工具能够在正确的位置跳转到定义)。class{}
0赞 jonrsharpe 11/5/2023
你这样做,使用泛型来表达输入和输出类型之间的关系(例如,如果您不熟悉,请参阅例如 typescriptlang.org/docs/handbook/2/generics.html)。在 JSDoc 中,这用 .如果您在应用时遇到问题,而这不在您发布的内容中,请展示它。@template

答:

-2赞 A_mohii_ 11/11/2023 #1
/**
 * 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();

评论

1赞 Mike 'Pomax' Kamermans 11/11/2023
Could you please update your answer so that it shows off what you describe with "Then, you can use the @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.typeinterface
0赞 Mike 'Pomax' Kamermans 11/11/2023
(Also note that the format for @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
0赞 A_mohii_ 11/11/2023
Yes Definitely I update my answer.
3赞 Mike 'Pomax' Kamermans 11/11/2023 #2

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()createClientClassinstance.test()CustomClass)

0赞 Julio Di Egidio 11/14/2023 #3

The return type can only be properly defined if , or a corresponding (the equivalent of an interface), is not hidden inside .ClientClasstypedefcreateClientClass

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.typedefcreateClientClass

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.