Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document class types/constructor types #1349

Open
tunderdomb opened this issue Apr 19, 2017 · 40 comments
Open

Document class types/constructor types #1349

tunderdomb opened this issue Apr 19, 2017 · 40 comments

Comments

@tunderdomb
Copy link

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

@CMCDragonkai
Copy link

Is Function<View> currently the only one acceptable?

@thw0rted
Copy link

thw0rted commented Aug 1, 2017

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 ☹️

@thorn0
Copy link

thorn0 commented Aug 30, 2017

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.

@cancerberoSgx
Copy link

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

@thw0rted
Copy link

thw0rted commented Nov 6, 2017

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.

@cancerberoSgx
Copy link

@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})
}

@cancerberoSgx
Copy link

Done ! :) https://github.com/cancerberoSgx/jsdoc-typeof-plugin ... Now I can have types that refer classes, something very very basic that jsdoc should support.

@dcleao
Copy link

dcleao commented Nov 11, 2017

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

@cancerberoSgx
Copy link

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

@brettz9
Copy link

brettz9 commented Apr 17, 2019

+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).

@73rhodes
Copy link

73rhodes commented Jun 24, 2019

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

@cancerberoSgx
Copy link

cancerberoSgx commented Jul 4, 2019

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

@likerRr

This comment was marked as spam.

@icsaba
Copy link

icsaba commented Apr 24, 2020

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) {}

@imjordanxd

This comment was marked as spam.

@aaclayton

This comment was marked as spam.

@wmertens
Copy link

@aaclayton is typeof not working for you?

@jespertheend
Copy link

jespertheend commented Feb 15, 2022

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)
}

(Playground link)

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)
}

(Playground link)

But types will be less strict, since the data argument now takes anything.

@aaclayton
Copy link

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

@wmertens
Copy link

Ah right - JSDoc can't handle typescript syntax, makes sense.

@brettz9
Copy link

brettz9 commented Feb 24, 2022

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

@thw0rted
Copy link

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

@aaclayton
Copy link

Am I reading these comments correctly as a recommendation that, even for pure JS projects, it would be advisable to:

  1. Use TypeScript style documentation syntax
  2. Generate TypeScript .d.ts files
  3. Build the JSDoc off those .d.ts files 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.

@brettz9
Copy link

brettz9 commented Feb 24, 2022

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

@thw0rted
Copy link

thw0rted commented Feb 24, 2022

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.

@brettz9
Copy link

brettz9 commented Feb 24, 2022

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.

@thw0rted
Copy link

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.

@frank-dspeed
Copy link

frank-dspeed commented Apr 12, 2022

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

@thw0rted
Copy link

thw0rted commented Apr 12, 2022

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 😳

@brettz9
Copy link

brettz9 commented Apr 12, 2022

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.

@thw0rted
Copy link

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.

@aaclayton
Copy link

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.

@thw0rted
Copy link

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.

@aaclayton
Copy link

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.

@thw0rted
Copy link

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.

@frank-dspeed
Copy link

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.

@ferrao
Copy link

ferrao commented Jun 2, 2022

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.

@frank-dspeed
Copy link

frank-dspeed commented Jun 3, 2022

@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 

@frank-dspeed
Copy link

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 🍡

@aral
Copy link

aral commented Sep 19, 2023

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()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests