Document class types/constructor types
tunderdomb opened this issue Β· 41 comments
Right now I couldn't find a way to document that a function argument (a property, or anything really) is a class, or a constructor of a specific type.
Here's a condensed example of the problem:
class View {}
class SomeView extends View {}
/* *
* This is a factory function that instantiates a new view object
* from the given View class and the given data.
*
* @param {???} ViewClass a constructor/class that extends View
* @param {*} [data]
* @return View a new View instance
* */
function createView(ViewClass, data) {
return new ViewClass(data)
}
var something = createView(SomeView, {});How is it possible to tell that the type of ViewClass is a constructor/class and that it extends View?
What should go in the place of the question marks up there?
@param {View} thing this means that thing is a View instance, not a class
@param {Function<View>} thing this doesn't really mean that thing is a class that extends View
@param {typeof View} thing logically this is what we're looking for, but frankly I've never seen this one used before, not in the docs at least
Is Function<View> currently the only one acceptable?
In VSCode, the assignment works (passing a constructor/class to a function expecting a Function<SuperClassType>) but the new is flagged with
[js] Cannot use 'new' with an expression whose type lacks a call or construct signature.
This is keeping me from using VSCode's static type checking
Closure Compiler has a syntax for this: https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler#function-new-type
And TypeScript / VS Code even understands it.
I'm using jsdoc together with typescript since it let's me to write plain old javascript and do the typechecking against the jsdoc. The syntax:
@property {typeof SomeClass} SomeClass
seems to be working fine in that environment. In my case (and i think is a very common case) I'm documenting what a native library provides. The module is exporting an object which properties are classes so this was mandatory. Unfortunately this is not valid jsdoc code and I dont have a way to exclude some comments from the jsdoc parser :(
I think this is very very important
Have you tried the {function(new:SomeClass)} syntax from @thorn0 's link? Also, have you considered moving your module declarations out to a .d.ts file instead? I prefer documenting JS inline where possible, but in your case it might be more flexible to describe the native library in actual Typescript.
@thw0rted that would work only in the case that I do new SomeClass but not in the case I want to call static methods / static members (example below). What we really needs is typeof that BTW is supported by typescript although not documented. I'M WRITING A JSDOC PLUGIN TO CHANGE {typeof A} to {A} or to {Class<A>} so at least jsdoc won't fail but IMO this should be solved in jsdoc without plugins. FWI, I already implemented a plugin for preventing some comments to be parsed by jsdoc (https://github.com/cancerberoSgx/jsdoc-prevent-comment-parse-plugin) so I'm not blocked anymore, but I really want this information to be exported in the docs so I need this other plugin. Will let you know when done.
Example of static methods in which {function(new:SomeClass)} wont work:
/**@class */
class SomeClass(){
/**
* Extends the current component to generate a child one
* @param {*} componentDefinition Any object with properties/methods that will be used to generate the Component that will be returned
* @return {*}
* @static
*/
static extend(componentDefinition) {
return null
}
}
.....
/**
@param {function(new:SomeClass)} Class
@return {*}
*/
function userCodeDONTwork(Class){
return Class.extend({foo: 1})
}
/**
@param {typeof SomeClass} Class
@return {*}
*/
function userCodeWorks(Class){
return Class.extend({foo: 1})
}
Done ! :) https://github.com/cancerberoSgx/jsdoc-typeof-plugin ... Now I can have types that refer classes, something very very basic that jsdoc should support.
This feature is highly missed. At my company, we use the convention Class.<A> to document constructors. This helps conveying the right information on the documentation, but is not recognized by tools.
Using the proposed typeof A works for IntelliJ (https://youtrack.jetbrains.com/issue/WEB-17325) but, unfortunately, causes eslint to start failing doclet parsing...
@dcleao in my team we also started using Class but that won't work with typechecking validation in editors like webstorm, vscode, atom, etc. I think typeof is not a proprietary thing of webstorm or vscode but is more like a typescript / closure compiler "standard". IN the case of eslint, probably you should issue that team or ask them to support that as an enhancement .
+1 for typeof. In my case, I'm also interested in a future/Typescript-compatible way for pointing to an argument supporting a @callback signature or a specific method of an @interface (and I am not yet clear on whether Typescript supports this).
+1 for @param {typeof Foo} as per typescript & closure compiler. Not being able to specify a type of constructor / class and reference it's static methods is kind of a show-stopper for me right now. I'd love to see any solution that keeps both the closure compiler and JSDoc happy.
I think today TypeScript is the defacto "jsdoc" standard. Great part of jsdoc grammar is currently defined in the language itself and new jsdoc grammar for type-related things like this too.
Since is a formal grammar definition, IMO people will tend to move away from this project's grammar, particularly for type related semantics.
The TypeScript AST for
/**
* @returns {typeof C}
*/
function f(){}
is something like this - sorry for the indentation
FunctionDeclaration
JSDocComment
JSDocReturnTag
Identifier
JSDocTypeExpression
OpenBraceToken
TypeQuery
TypeOfKeyword <----- the same node kind than in actual typescript code
Identifier <----- this is the name of a class or typedef
CloseBraceToken
FunctionKeyword
Since the objective of TypeScript is typecheck, they needed to create that syntax and used the same grammar that's already present in TypeScript for that. In other situations they use syntax already in TS for jsdocs, for example, generics - they don't support @template but the well known syntax {Array}.
On the other hand TS syntax don't support documentation-related semantic like @link, @example, etc.
My point is, for those who want typechecking and already have jsdocs comments, give TypeScript a try. No modifications are needed in a js project, can be done gradually mixing js and ts files, and compilation / Type errors can be configured pretty flexible. Also jsdocs itself ends up being simpler since in TS, jsdocs don't have types references or to explain what's you are documenting @Class @method are no needed. That simplify the comments a lot.
I wonder if there are any plans in this project to catch up or support more of current TypeScript jsdocs syntax.
An interesting proiect could be parse js code using TypeScript compiler and generate a AST compatible with the syntax of this project (so I can use themes, etc to document TS/JS projects).
Any updates on this?
how to add restrictions like i am waiting for classes that extends certain other classes?
for example, my function accepts class parameter that extends B:
/** @param {any extends B} */
function foo(ClassName) {}
any updates on this? I want to document a mixin that adds a function to any class passed in and cannot seem to find a way to document this whilst keeping the IntelliSense?
Still key missing functionality to document a parameter or variable whose type is a class definition. Pretty basic stuff that is badly needed.
@aaclayton is typeof not working for you?
edit: ah forget about this, I thought I was commenting on the TypeScript repo.
To answer the original question. You can type functions like createView() like so:
/**
* @template {View} T
* @template {any[]} U
* @param {new (...args: U) => T} ViewClass
* @param {U} [data]
* @return {T}
*/
function createView(ViewClass, ...data) {
return new ViewClass(...data)
}If you do it like this, parameter hints for the createView() function will display the parameters of the constructor that was passed in.
Or if you don't wish to use @template:
/**
* @param {new (...args: any[]) => View} ViewClass
* @param {any[]} [data]
* @return {View}
*/
function createView(ViewClass, ...data) {
return new ViewClass(...data)
}But types will be less strict, since the data argument now takes anything.
@aaclayton is typeof not working for you?
@wmertens it might work with a plugin, but it does not work natively in JSDoc, if I annotate something as @type {typeof DataModel} or @param {typeof DataModel} (where DataModel is an abstract class in my environment) there are errors when building the documents as follows:
ERROR: Unable to parse a tag's type expression for source file {file path} in line {line number} ...
Invalid type expression "typeof DataModel": Expected "|" but "D" found.
Ah right - JSDoc can't handle typescript syntax, makes sense.
Imho, JSDoc just isn't moving quickly enough to update its type parser to work with TypeScript's flavor of JSDoc. At the most basic level, the community needs something like TypeScript's import() innovation now, not to mention the rest of TypeScript's robust additions. Given TypeScript's capabilities in and of itself, and the ecosystem that has built up around it (including use of TypeScript-flavored JSDoc within plain JavaScript files) I think it's time or at least getting to be time to recognize TypeScript's JSDoc flavor as the new official one, and orient documentation tools around it's flavor (and hopefully see TypeScript fix its remaining jsdoc issues, particularly important, to my sense, giving proper import and export control from within JS+JSDoc alone: microsoft/TypeScript#22126 and microsoft/TypeScript#46011 ). On the community side jsdoc-type-pratt-parser could I think use some love in getting full TypeScript support, and then perhaps jsdoc could use that instead of catharsis (jsdoc-type-pratt-parser apparently supports all of its tests already too).
I think it's fair to say that JSDoc has already tacitly passed the torch to Typescript. The mailing list has 5 new threads since 2017, which is coincidentally the last time the official website was updated. PR #1440 was the most recent one I could find that was not a dependency/compatibility or documentation update, also, funny enough, from 2017.
There have been a couple of "is JSDoc dead" issues here, and eventually @hegemonic shows up to say that he's back and working on it again, but if there were PRs that added new features or syntax I couldn't find them. I think it's critically important to say: I'm not complaining about that! This is an open source project, with a budget of 0.00 full-time employees. I honestly appreciate the work people put into this very important project! But on the internet, if you don't swim you die, and JSDoc has not been swimming for half a decade. IMHO, the right move for any tools built on JSDoc would be to treat "Typescript flavor" as the one true syntax. (Sure would be nice if MS went ahead and adopted this project in the process...)
Am I reading these comments correctly as a recommendation that, even for pure JS projects, it would be advisable to:
- Use TypeScript style documentation syntax
- Generate TypeScript
.d.tsfiles - Build the JSDoc off those
.d.tsfiles rather than off the source.js
I suppose its worth a try, it's unfortunate that type of intermediary step is necessary but if it produces both valid IDE inspections and valid HTML documentation then I would view that as a victory.
Only steps 1 and 2 should be necessary, and step 2 only if you wish other projects to use your code, noting that for step 1, TypeScript-style documentation syntax is JSDoc, it's just a different dialect of JSDoc as far as which JSDoc tags have formally defined behavior, what the types are (there are many more available in TypeScript flavor), and what the allowable syntax is (the TypeScript team tried to hew closely to JSDoc, but they avoided @module in favor of allowing import() of third party types and they don't yet support certain tags in a type-aware way--though for doc tools it might not matter). See https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html for some details.
You can already use tools like typedoc (based on a tsdoc standard) to build docs out of plain JS and TypeScript-flavored JSDoc.
For more info, I just added a Gist summary of my experiences with some references at https://gist.github.com/brettz9/17bcbfa2b7c0e4ddaac4604da4490e23
Just to check, do you mean only steps 1 and 2, or only steps 1 and 3? If typedoc supports vanilla JS -> HTML directly, it shouldn't be necessary to create intermediate .d.ts files, right?
ETA: to clarify what I said above, I'm not sure if TS-flavored-JSDoc is a totally great experience for vanilla JS projects today, but it seems likely to be the best solution going forward. I hope that TS is willing to pick up the mantle and be the standard, even if you're not writing TS, but I recognize that there may still be gaps. I just don't think that sticking to a 5+ year old spec, while JS/ES continues to evolve around it, is really sustainable for anyone.
Step 2 is optional. You need a declaration file if you want third parties to use your code in a type-aware manner (unless DefinitelyTyped has already made type files on your behalf in which case your users can just use the types from DefinitelyTyped).
I haven't been building docs lately; you'd have to check out typedoc (or maybe better-docs (a tutorial). But I think you may be able to use the noEmit setting in your tsconfig.json file to avoid building declaration files while still having a doc tool know what to do based on the JSDoc +JS alone.
Tools that build docs using TypeScript should, I believe, be able to parse JSDoc along the way without having to first build physical declaration files, since TypeScript is capable of processing them on their own. So I did mean 1 and 2 should be necessary, and only step 2 if you need to export types for other projects.
I see. I look at maintaining your own typings (assuming we're talking about publishing, say, an open-source library authored in vanilla JS) as a nice to have, but not strictly necessary, and I wouldn't specifically couple it to documentation. I'd be very happy if we can just get to a place where a project that uses jsdoc to generate docs today, could spend 15 minutes hooking up e.g. typedoc or better-docs as a drop in replacement and get roughly the same output. Adding generated TS typings files to the process would be an awesome bonus.
Let me maybe close this issue i had success with the following method
const myClass = class {}
const fn = (/** @type {new () => any } takeClass) => {}so the secret source is
this type: {new () => any }
const myClass = class {}
/**
* @param {new () => myClass| } takeClass
*/
const fn = (takeClass) => {
myClass() // => correctly errors that it needs and should get called with new
} tested with current typescript 4.7 note that we explicit say => myClass so only myClass is valid in fn
{new () => myClass | new () => any} is a more lexed version
I'm 99% sure your syntax ({new() => myClass}) is not JSDoc, but actually TypeScript. There's a lot of confusion because so many people use tools that interpret JSDoc comments but are actually passing it through the TypeScript compiler / language-service under the hood. Same thing keeps happening with e.g. the type-union (|) operator.
ETA: c.f. my many comments about this roughly 2-3 pages upthread, which I wrote 2 months ago then promptly forgot π³
It is true that those types are not accepted by JSDoc's type parser, but the fact is that the development pace of TypeScript to accommodate all kinds of important needs is much faster and broader than for this particular tool, however appreciated it is for having advanced the phenomenon and served/serving as a useful tool.
Note that one does not need to use TypeScript syntax in order to use the types within JSDoc comments. One can use plain JavaScript and use tools like typedoc to get documentation built similar to JSDoc, without needing a single line of TypeScript-specific syntax. (there are some current imperfections using JSDoc alone to export definition files, but plain JSDoc doesn't allow this anyways).
TypeScript's form can in fact, in a sense be said to be JSDoc, as it conforms in the general structure of JSDoc blocks, but it merely follows a special dialect in its types. (As far as other forms of JSDoc "vocabulary", TypeScript's checker won't complain about tags it doesn't recognize. Although there are some tags it would ideally support but doesn't, the real "incompatibility" is just about types.)
In eslint-plugin-jsdoc, we offer plain jsdoc mode, typescript mode, and closure mode (Closure has a few of its own quirks). But we are strongly considering switching our tool to the TypeScript flavor by default given that it can be used for more than just documentation (type checking for one) and can still work with plain JavaScript too.
I agree with Brett's comments about switching, insofar as TypeScript is active and JSDoc pretty much isn't. My point was that there are still tools that only support vanilla JSDoc and Frank needs to be aware that his TS-specific syntax could break them. It's just rehashing the discussion upthread, but I wanted to make sure Frank (and any future readers) noticed.
For those advocating "just switch to TypeScript for docs generation", it's not simple to do.
Supposing you have an existing codebase which is pure JS documented using JSDoc. You can install typedoc and configure tsconfig.json file, but getting typedoc to actually generate docs for your existing JS code? Good luck. Best case scenario it works if you're using very simple javascript, but bring ES modules into the picture and things start to look bleak fast.
There's no tutorial to follow for doing this, just TypeScript advocates who misleadingly say its easy.
I hope I didn't give the impression that "it's easy" above. The problem is that JSDoc is not being updated to reflect the rapidly evolving vanilla-JS (ES) ecosystem. If you have legacy vanilla JSDoc tooling and want to keep growing your project, unfortunately you don't have any "easy" choice. You can pick trying to hack around the missing support in JSDoc for features like the one that prompted this issue, which is hard, or you can pick migrating to a better supported doc flavor like TS, which is also hard.
I personally think that the (hard!) migration to TS has a brighter future, so that's what I generally recommend, with the understanding that everybody's situation is different and it may not be the best fit for you.
The main friction, as I see it, is that it's not a simple case of a docs migration. The docstrings in our code are (largely) already typescript compliant, but getting an infrastructure set up to actually build documentation HTML using typedoc from those files was not something I was able to do after spending around 3 hours working through tutorials on https://typedoc.org/, https://typedoc.org/, and desperately googling to address any errors. At the end of the day I wasn't able to actually build any docs at all which makes this seem to me more in the "not possible" category rather than in the "hard work" category.
Would be happy to be proven wrong, but this probably isn't the right place to dive into it.
I agree this isn't the place, but https://github.com/TypeStrong/typedoc/issues looks pretty active -- 86 open out of ~1400 total, lots resolved within the last week, and the first "closed issues" page dates back about a month. I really, honestly don't want this whole thread to turn into JSDoc-bashing, but compare that with this repo, where the first page of closed issues goes back fully 2 years.
I'll take working with a vibrant community to get through a painful initial setup over sticking with a stagnant project any day of the week.
I would also opt for the merge and setting up typescript to emit types and declarations as also docs from pure JS is indeed the only thing that i do and it is not so hard anymore it has much improved but you need to learn tsconfig a bit.
Just my 5cent
it makes not sense to maintain the old jsdoc type behavior it should algin with typescript. Anything else will have no future.
I hope I didn't give the impression that "it's easy" above. The problem is that JSDoc is not being updated to reflect the rapidly evolving vanilla-JS (ES) ecosystem.
Classes or constructor functions as either return types or function arguments are both common and idiomatic in JavaScript.
I fail to understand how/why jsdoc does not handle this basic use case.
@ferrao because the use case is not basic in JS/ECMAScript anything can be anything and anything is anything
a more easy explainer would maybe be the JS Fundamental Design says everything in JS is a Object even a string
"I am Object".toString()A Object is defined as a UTFString Indexed Object that can have nested Object eg: Methods Functions Symbols
and now comes the horror a Object can return Anything so you Class can be Anything
import EventEmitter from './EventEmitter.js'
const aha = class extends EventEmitter {
constructor() {
super();
return "Ups i Did it again"
}
}
new aha() === "Ups i Did it again"let me give you a example of a class that changes over time after lets say 2 process iterations it will get a property
import EventEmitter from './EventEmitter.js'
const lazy = class extends EventEmitter {
constructor() {
super();
this.later = "Ups i Did it again"
setTimeout(()=> this.later = 5, 100)
}
}
const l = new lazy()
l.later === "Ups i Did it again"
setTimeout(()=>console.log(l.later), 300) // l.later === 5 maybe your all interrested into this https://github.com/tc39/proposal-type-annotations
we are trying to get type annotations parsed as comments directly in ECMAScript it self π‘
Iβm rather surprised this is still an issue in 2023.
Currently, it doesnβt seem possible to use JSDoc and have the TypeScript LSP understand when youβre referring to a class and when youβre referring to an instance.
e.g., In a simplified example from my use case, where I need to provide type information for a global object, the only way I have found so far to do this is to provide separate types for the class and its instances. Needless to say, not a great solution and very confusing for those having to consume this during authoring:
index.d.ts:
declare class Session {
createdAt: Date
authenticated: boolean
hasExpired (): boolean
}globals.js
const Session = /** @type {typeof import('./index.d.ts').Session } */ (globalThis.kitten.Session)
const SessionInstance = new Session()
// Have to use SessionInstance here
const _db = /** @type {{
sessions: Object.<string, SessionInstance>
}} */ (_kitten._db)
// And Session (the actual class) here.
const session = new Session()In case you want to use TypeScript syntax not supported by JSDoc, but still keep the ability to generate the docs without ERROR: Unable to parse a tag's type expression, one workaround is to use @ignore @typedef:
class Data {}
const data = new Data();
class View {
/**
* @param {Data} data
*/
constructor(data) {}
}
// using typeof
/** @ignore @typedef {typeof View} ViewClass */
/** @type {ViewClass} */
const viewClass = View;
const view = new viewClass(data);
// using new
/** @ignore @typedef {new (data: Data) => View} NewableView */
/** @type {NewableView} */
const newableView = View;
const anotherView = new newableView(data);
// in a function parameter
/**
* @param {ViewClass} view Class of {@link View} to create
* @param {Data} data
* @returns {View}
*/
function createView(view, data) {
return new view(data);
}this fixes VSCode (TypeScript) type checking AND you can still generate JSDoc fine (except for WARNING: The @ignore tag does not permit a value; the value will be ignored.). The links to ViewClass will obviously not be created, but as long as you name and describe things clearly, name ViewClass still provides more context than just Class.