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

Clarification of how to properly include typedefs in documentation built for an ES module. #2044

Closed
aaclayton opened this issue Aug 28, 2022 · 14 comments
Labels
question Question about functionality

Comments

@aaclayton
Copy link

aaclayton commented Aug 28, 2022

Search terms

typedef, esmodule, class, "referenced but not included in the documentation"

Question

I am using typedoc to document an ESModule. An example file in that module defines a class and its constructor parameter(s) as follows (this is a simplified example):

File 1: Class Declaration - some-class.mjs

/**
 * @typedef {Object} SomeDataStructure
 * @property {string} foo      A string
 * @property {number} bar   A number
 */

/**
 * A class that does a thing
 */
export default class SomeClass {
  /**
   * Construct an instance of SomeClass using provided data
   * @param {SomeDataStructure} data   Input data
   */
  constructor(data) {
    this.data = data;
  }
}

The documentation is built by targeting another file which is the entry-point that aggregates exports for different components of the module:

File 2: Module Entrypoint - module.mjs

export {default as SomeClass} from "./some-class.mjs"

What Goes Wrong?

When trying to build documentation for the module the following warning is displayed:

Warning: SomeDataStructure, defined at common/some-class.mjs:2, is referenced by foundry.SomeClass.data but not included in the documentation.

Documentation is built for SomeClass, but the data type of its constructor parameter is not documented nor is the SomeDataStructure typedef available for inspection in the resulting documentation.

image

I realize this is a somewhat uncommon use case since the source of the module is not TypeScript but rather an ESModule directly, but this use case is portrayed as being supported by https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#typedef-callback-and-param.

Is this a typedoc bug? Is this a problem with the way I have structured the module code? Thanks for your guidance.

@aaclayton aaclayton added the question Question about functionality label Aug 28, 2022
@Gerrit0
Copy link
Collaborator

Gerrit0 commented Aug 31, 2022

This is, accidentally I think, the best argument I have seen for TypeDoc "automatically" including non-exported types...

TypeDoc is working as intended here - your module entry point re-exports SomeClass, but does not export SomeDataStructure -- it isn't possible for a TS consumer of the library to do:

import { type SomeDataStructure } from "your-module"

Because TypeDoc only walks exports from your entry point(s), and SomeDataStructure isn't exported from the entry point, it isn't included in your documentation. It doesn't look like there's a re-export equivalent for JSDoc, so I'd probably recommend a plugin in this case...

Two commonly used plugins are:

@aaclayton
Copy link
Author

Thanks @Gerrit0 - I think that https://www.npmjs.com/package/typedoc-plugin-missing-exports can work here, but it does come with some limitations and flaws.

I wonder whether you think this should be a supported usage for TSDoc/typedoc? If so, is there an avenue to pursue for advocating a change to the core behavior?

A limitation of the JSDoc @typedef is that it cannot be declared as or interpreted as a module export. This is a documentation problem that could perhaps be solved with a documentation solution. Is it a reasonable feature request that typedoc might interpret @typedef as an an exported symbol and include them in generated documentation accordingly as if they were exported?

If that feature request were made, would it be a typedoc request, or would it be an upstream request to typescript? Do you know of any existing discussion on this matter? I am certain it has come up as I don't think my use case here is that unusual.

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Sep 3, 2022

A limitation of the JSDoc @typedef is that it cannot be declared as or interpreted as a module export.

This is not true. @typedef declarations in a module are considered exported.

// @filename: jsdoc.js
/** @typedef {string} MyString */
export const abc = 123

// @filename: test.ts
import { MyString, abc } from "./jsdoc.js"
export const x: MyString = abc // Error: Type 'number' is not assignable to type 'string'.

The problem arises because TypeScript doesn't have a clean way to re-export types via JSDoc. The above is equivalent to:

// @filename: jsdoc.ts
export type MyString = string
export const abc = 123

// @filename: test.ts
import { MyString, abc } from "./jsdoc.js"
export const x: MyString = abc

MyString isn't exported from test.ts, so TypeDoc would correctly not include it in the documentation as it is deemed internal. I'm not sure of a really good solution to this, there's no convenient export { MyString } for types in JSDoc.

TypeDoc could support this better by detecting type "re-exports" done with /** @typedef {import("./jsdoc").MyString} MyString */ rather than treating them as the type system does (this is equivalent to type MyString = import("./jsdoc").MyString, which introduces a new symbol, not an aliased symbol), but this isn't ideal either, since it requires including the import specifier and specifying the name twice in every type re-export. This would result in a behavior difference from the equivalent TS, but I think it's probably worth adding...

A "real" type re-export feature would have to come as an upstream feature from TypeScript. microsoft/TypeScript#22160 is vaguely related, but really highlights that using JSDoc for types is truly a second class citizen in the TS world...

@aaclayton
Copy link
Author

Thanks for your clarification @Gerrit0 - there's definitely some complexity and competing objectives here so I understand why this is a sticky situation.

At the end of the day I think it's uncontroversial that a @typedef defined and used within the scope of a module should be included in the resulting documentation (regardless of how that happens) - but it seems there a gap where it could be handled at the typescript level but the TS folks don't feel powerfully about supporting second-class JSDoc features (understandable).

The other path would be more of a pure documentation solution where the typedef - under certain circumstances - could still be included in the generated docs even though it isn't explicitly exported. That seems perhaps like the path of less resistance and is similar to what the https://www.npmjs.com/package/typedoc-plugin-missing-exports plugin does but with a more specific and limited scope of impact.

It doesn't seem too controversial to me that typedocs might support the following option:

Include documentation for non-exported typedefs which are used by an exported symbol.

It doesn't seem like there are upstream technical blockers towards typedoc supporting such an option, so I hope it's a feature request you might consider adding to the roadmap.

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Sep 4, 2022

TypeDoc's goal is to create documentation which is consistent with what is available to your module's consumers. The missing exports plugin provides an escape hatch for those who can't/won't export the types necessary for their users. I sympathize with people stuck using JSDoc types (I'm one of them at work atm...) but really don't want to compromise that design goal...

@aaclayton
Copy link
Author

Thanks for your perspective, I do have a disagreement with that statement though:

When my module explicitly exports a class via export default SomeClass (in my original example), documentation of the SomeDataStructure is absolutely part of the "documentation which is consistent with what is available to your module's consumers".

The type of the data object the class takes as its constructor is an essential part of the documentation for that explicitly exported class, regardless of whether the typedef itself was explicitly exported (which is not possible in JSDoc) or not.

Gerrit0 added a commit that referenced this issue Sep 5, 2022
@Gerrit0
Copy link
Collaborator

Gerrit0 commented Sep 5, 2022

regardless of whether the typedef itself was explicitly exported

This is where we disagree, since the user can't import { type SomeDataStructure } from "yourLib". However, I do believe that detecting @typedefs which are "exports" of other type declarations is a good idea, and have added support for that with 0c1b9c8

@aaclayton
Copy link
Author

Is the purpose of API documentation not to inform the user how to use the exported classes and functions of a module? What use is API documentation if the docs don't tell you how to construct SomeClass in my example? Is this not a failure of the documentation that it provides no instruction here?

It feels like your perspective loses sight of the fundamental purpose for documentation in favor of turning a (surmountable) technical limitation into a philosophical line in the sand that ultimately doesn't serve the end user.

I hope my feedback on this area is useful to you and that your perspective may shift over time, but either way thank you for your time spent looking into this matter and for your development of this codebase.

@Oblarg
Copy link

Oblarg commented Oct 30, 2022

I agree strongly w/ @aaclayton here; if knowledge of the shape is necessary for use of the library then the shape should be documented in the API docs. Whether or not the type is made available for explicit use in consuming code isn't really relevant; it's an implicit dependency and the user needs to know the shape either way.

The existing missing-exports plugin works alright for this, but has some annoying bugs around inline imports. It'd be really helpful if this were first-class behavior.

@Gerrit0 Gerrit0 closed this as completed Dec 11, 2022
@aaclayton
Copy link
Author

I think it would be a shame for the discussion in this thread to be treated as "closed" even though the original question was answered. I really believe this represents a shortcoming in the current functionality of documentation generators and it would be great to keep this issue open to reflect the possibility to improve the software, even if those improvements are not prioritized in the short term.

@aaclayton
Copy link
Author

I continue to think it's a mistake for this issue to be treated as "closed". This is a major limitation which cripples the functionality of typedoc as a documentation generator with JSDoc interoperability. Please reconsider whether more discussion can occur here which might lead to a potential solution path.

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Oct 15, 2023

If this were to be implemented in TypeDoc itself, rather than typedoc-plugin-missing-exports, it would use (roughly) the same implementation, which makes "major limitation" seem like a major overstatement. The functionality you want is already available... If something is wrong with that plugin, issues should be filed there, I certainly don't want to bring functionality into TypeDoc itself which is half baked.

A large part of my resistance to this feature is that it encourages people to be sloppy with both what is exposed by their library and what they export. Requiring that documented members be exported means that:

  1. If I accidentally expose some internal member that shouldn't be part of the public API, I get warnings about it rather than it implicitly becoming part of the public API.
  2. The documentation matches what's actually usable in the library. As a user, there's nothing more frustrating than being promised that some API exists, and then not being able to use it (see: chatgpt hallucinating APIs)

Is it frustrating for your JSDoc use case? Absolutely, but the plugin does exist, and frankly, JSDoc is a second class citizen in the TypeScript world...

(having this option live out in a plugin lets me see how many people are actually using it, which is higher than I expected at 6% of users, but well below typedoc-plugin-markdown numbers, which certainly doesn't belong in TypeDoc itself, popularity isn't everything)

@aaclayton
Copy link
Author

Thank you for the response @Gerrit0 - we are using the typedoc-plugin-missing-exports package and it is failing to properly document a number of our classes. I'll prepare some simple examples and get back to you in a few days. I appreciate your willingness to consider those. Treating them as bugs in typedoc-plugin-missing-exports could also be a satisfactory solution so once I've shared that please let me know if you would recommend opening issues there or whether this is the best place.

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Oct 15, 2023

Bugs over there are the best place, that way they're more easily tracked, thanks!

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

No branches or pull requests

3 participants