Tsdoc: RFC: Support for dot syntax on @param

Created on 25 May 2018  路  13Comments  路  Source: microsoft/tsdoc

I'm currently working with d.ts files where methods taking object parameters are documented with the following approach:

/**
* Method description.
* @param options addItemOptions - e.g. { id: 1234, title: "someTitle" }
*             id: The unique identifier.
*             title: The title to assign.
**/

It's challenging to parse and auto-generate documentation for these parameters given this approach. Ideally there'd be support for dot syntax e.g.:

@param options.id - The unique identifier.
@param options.title - The title to assign.
octogonz-parser-backlog request for comments

Most helpful comment

What's more of a pain to document is destructured arguments:

/**
 * Call home.
 * @remarks
 * This texts Mom a message.
 * @param message - The message to send to Mom
 * @param ? - The number of seconds to delay calling home
 * @public
 */
function callHome (message: string, { delay = 0 }: { delay?: number } = {}) {
  // TODO: send a text to Mom!
  throw new Error('Unimplemented')
}

All 13 comments

For APIs intended for third parties, generally we recommend to give a name to the interface, for example:

/** Options for {@link ItemCollection.add} */
interface IAddItemOptions {
  /** the identifier */
  id: string;
  /** a title, not to exceed 40 characters */
  title: string;
}

class ItemCollection {
  /**
   *  Adds an item to the collection
   * @param options - information about the item to be added
   */
  public add(options: IAddItemOptions): void {
  }
}

Naming the interface improves the structure of the documentation. It also helps developers when they need to work with the object independently, for example:

const options: IAddItemOptions = this.getOptions();
collection1.add(options);
collection2.add(options);

That said, I believe TypeDoc supports the @param options.id notation. If people find it useful, we could include it in the TSDoc standard.

What's the long term plan for TSDoc? Will we be able to use it to annotate vanilla JavaScript like the current JSDoc support? In those cases the it's crucial to be able to declare an interface in the comments or document @param properties.

@seanpoulter I moved your question into a separate issue, since it's a separate topic.

An additional point to consider when reviewing the dot syntax is the values might not be valid identifiers. When describing a tuple type, it would be great to be able to be able to use a.0 and a.1.

I wouldn't put this functionality in the initial version of TSDoc, because we could go forever like that and not release anything as it's been quite some time already. I'd rather have a working version using non-exported interfaces to document function parameters rather than postpone release by whatever amount of time with more functionality.

@pgonzal, if you add me to admins of the project (or whatever role I could be), I could structure things with releases and add initial versions to the issues. If we get arguments saying that something is super important and should be done from the get go, we can always change priorities. But it would be clear to everyone what are we aiming at.

What's more of a pain to document is destructured arguments:

/**
 * Call home.
 * @remarks
 * This texts Mom a message.
 * @param message - The message to send to Mom
 * @param ? - The number of seconds to delay calling home
 * @public
 */
function callHome (message: string, { delay = 0 }: { delay?: number } = {}) {
  // TODO: send a text to Mom!
  throw new Error('Unimplemented')
}

@dschnare do people commonly do this in a real world public API?

This practice seems to be entangling the internal implementation details of callHome() with its public API signature. It makes things more concise for the person who's writing the code, but at the expense of hindering our ability to help other people read and understand the code. One of my personal soapboxes is: Reading > Writing. (Writing code is fun and easy. But enabling others to understand and extend a code base is hard work, and a more important goal.)

From a documentation standpoint, destructuring has some downsides:

  • There's no name that we can use to discuss the second parameter of this function.
  • If a caller wants to construct and reuse that parameter, they can't easily declare its type
  • If we later want to deprecate the delay option, or write examples for it, there's no place to put that

For an external-facing API, my own team would normally want to convert it to something like this:

/** Options for the `callHome()` API */
export interface ICallHomeOptions { 
  /** 
   * The number of seconds to delay calling home
   * @defaultValue 0 
   */
  delay?: number;
};

/**
 * Call home.
 * 
 * @remarks
 * This texts Mom a message.  If you want to delay the message,
 * consider using {@link ICallHomeOptions.delay}.
 * @param message - The message to send to Mom
 * @param options - Additional options
 * @public
 */
function callHome (message: string, options?: ICallHomeOptions = {}) {
  // TODO: send a text to Mom!
  throw new Error('Unimplemented')
}

So now a caller can do this:

// Using this:
function getStandardOptions(scenario: Scenario): ICallHomeOptions {
  . . .
}

callHome(message, this.getStandardOptions(Scenario.Intranet));

And someone can open a GitHub issue complaining that ICallHomeOptions.delay is broken, and we know what they're talking about.

@dschnare do people commonly do this in a real world public API?

I have struggled with this use case trying to add type information to a vanilla JS for some time. I wouldn't go so far as to call it the "public API" but this is common practice when consuming props in a React component. I've found the first argument destructured ~450 times in ~300 files in an e-commerce website I work on. If you have access to any other React repos, try searching for const [^ ]+ = \(\{.

Fair point. But how would you propose for TSDoc to handle that?

I agree with @pgonzal's comments. IMO it would be better to define an interface. Instead of destructuring in the function arguments, I would also recommend destructuring within the function body. I'm not sure if you were counting this in your react repo but I wanted to note that alternative which I find makes the function signature easier to understand.

It may be common not to create an interface for function arguments, however I don't think TSDoc should be complicated because of this pattern when there is a clear alternative.

/** Options for the `callHome()` API */
export interface ICallHomeOptions { 
  /** 
   * The number of seconds to delay calling home
   * @defaultValue 0 
   */
  delay?: number;
};

/**
 * Call home.
 * 
 * @remarks
 * This texts Mom a message.  If you want to delay the message,
 * consider using {@link ICallHomeOptions.delay}.
 * @param message - The message to send to Mom
 * @param options - Additional options
 * @public
 */
function callHome (message: string, options?: ICallHomeOptions = {}) {
  const { delay = 0 } = options;

  throw new Error('Unimplemented')
}

I think you've nailed it for the public API. 馃憣

I've been brainstorming variants but haven't converged on anything I like. I've shared the variants I've thought of below. If anything, you've won me over that adding the interface is the way to go. It's readable and reduces the complexity to parse things.


Brainstorming ... 馃挱

I've mashed this into a JSDoc comment in the past which doesn't work as you add members or descriptions:

/** @param {name: string, age: number} ? - A person */
const toExample = ({ name: n, age: a }) => `${n} - ${a}`;


Consider the case when you refactor a function that has too many arguments to use named arguments with default values. The dot notation is great for this case. You can see the repetition of * @param args. adds to the clutter -- the interface is looking better.

const bad = (a, b, c, d, e, ...rest) => {};
// This is called as: bad('foo', null, null, FORM.FOO_ID, 1)

/**
 * @param args
 * @param args.a - Placeholder
 * @param args.b - Accepted values
 * @param args.c - Validation functions
 * @param args.d - Field ID
 * @param args.e - Tab index
 */
const better = ({ a, b = null, c = null, d = null, e, ...rest }) => {}; 


It would be nice to remove the args. and have an anonymous argument. Naming things is hard. The ??? placeholder is really awkward and it's clear that this would look better expanded -- like the interface.

/**
 * @param message
 * @param ???.delay - The seconds ... etc
 */
const callHome = (message: string, { delay = 0 } = {}) => {};


If we got a little crazy, we could drop the parent object(s) from the annotation and only document the values after they're destructured and renamed and let the type inference pick up the slack:

/**
 * @param' n - First name
 * @param' a - Age
 */
const toExample = ({ name: (n: string), age: (a: int) }) => `${n} - ${a}`;

I would also like to see this implemented, as sometimes it doesn't make sense to introduce an interface for optional named parameters.

For reference, here is the documentation for the same feature in JSDoc: https://jsdoc.app/tags-param.html#parameters-with-properties

Seems reasonable. :+1:

Was this page helpful?
0 / 5 - 0 ratings

Related issues

octogonz picture octogonz  路  14Comments

octogonz picture octogonz  路  8Comments

AndrewCraswell picture AndrewCraswell  路  7Comments

vidartf picture vidartf  路  8Comments

stacey-gammon picture stacey-gammon  路  12Comments