typedoc icon indicating copy to clipboard operation
typedoc copied to clipboard

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

Open aaclayton opened this issue 3 years ago • 8 comments

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 avatar Aug 28 '22 16:08 aaclayton

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:

  • https://www.npmjs.com/package/typedoc-plugin-missing-exports
  • https://www.npmjs.com/package/typedoc-plugin-merge-modules

Gerrit0 avatar Aug 31 '22 03:08 Gerrit0

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.

aaclayton avatar Sep 01 '22 15:09 aaclayton

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. https://github.com/microsoft/TypeScript/issues/22160 is vaguely related, but really highlights that using JSDoc for types is truly a second class citizen in the TS world...

Gerrit0 avatar Sep 03 '22 02:09 Gerrit0

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.

aaclayton avatar Sep 03 '22 13:09 aaclayton

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

Gerrit0 avatar Sep 04 '22 02:09 Gerrit0

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.

aaclayton avatar Sep 04 '22 19:09 aaclayton

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 0c1b9c8e515e46013e650cf5f4d1e405b6eccdad

Gerrit0 avatar Sep 05 '22 23:09 Gerrit0

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.

aaclayton avatar Sep 05 '22 23:09 aaclayton

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.

Oblarg avatar Oct 30 '22 17:10 Oblarg

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 avatar Dec 11 '22 18:12 aaclayton

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.

aaclayton avatar Oct 11 '23 13:10 aaclayton

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)

Gerrit0 avatar Oct 15 '23 18:10 Gerrit0

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.

aaclayton avatar Oct 15 '23 18:10 aaclayton

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

Gerrit0 avatar Oct 15 '23 18:10 Gerrit0