Is there a way to use JSDoc with plain JavaScript (not TypeScript) to define the return type for a function like this, which creates a new class as output that's an extension of a user-provided class provided as function input?
/**
* 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();
The following technically "works" in that the typing isn't strictly speaking wrong, but will of course not contain any of the dynamic class's methods and fields that aren't in the 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`
The alternative seems even more problematic: if we pull out the dynamic class as a real class, then we run into inheritance problems because JS does not allow multiple inheritance on classes:
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.
};
}
Is this one of those things that JSDoc just can't do, or is there a JSDoc way to do what TS does with types and interfaces, without having to write "no-op" JS that captures the methods and properties of some class without ever getting used for anything other than type docs?
As suggested, I read the docs on generics and templates, but either I'm skipping over something or this isn't covered by the examples given, because with templates all I can come up with that yields a return type in the type hinting is:
/**
* @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();
Where the return type for the function is now flagged as implements U extends T
with the typing hint for ClientClass
being ClientClass
, and while bindServerSocket
resolves to the dynamic class method, test
does not resolve to the method of the class that was passed into the function.
(The same happens with @return {U extends T}
, where the typing can resolve functions from U, but not from T)
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 instance.bindServerSocket()
is defined in the createClientClass
function, but cannot tell that instance.test()
is defined in CustomClass
)