0

I've got code that spreads the returned object, and adds in a new property

It looks like this :

/**
 * it does the things
 * @param {Object} input object containing id and key
 * @param {Object.string} id unique id to associate with return value
 * @param {Object.string} key unique key used to get things
 * @returns {Object.<{???, id:string}>} the returned things and id.
 */
const doTheThings = ({id, key}) =>
  thingDoer(key).then(things => {
    ...things, // how do I document the properties of this
    id         // combining with this?
  })

I currently have ??? in the @returns part equal to things: *. but that makes it look like there will be a key called 'things' in the return, when there's not.

How do I document what things is? Does it change if thingDoer has a JSDoc of its own I can lean on? Preferably without involving typescript.

AncientSwordRage
  • 7,086
  • 19
  • 90
  • 173
  • Sounds like a generic here. You take `T` and return it enriched. Does it *always* add the same properties (the `things`) or can they be different? – VLAZ Nov 15 '21 at 13:41
  • @VLAZ except JavaScript doesn't have generics. When you say "Does it always add..." do you mean does the function `thingDoer` always return the same `things`? Yes, it's always the same shape, but the values might change. – AncientSwordRage Nov 15 '21 at 13:50
  • 1
    "*except JavaScript doesn't have generics*" but JSDoc has ;) Even if they aren't well documented. "*Yes, it's always the same shape, but the values might change.*" then perhaps a generic is an overkill. If it's always the same, maybe you just need [`@typedef`](https://jsdoc.app/tags-typedef.html). – VLAZ Nov 15 '21 at 13:52

1 Answers1

2

The fact that it is using spread syntax is irrelevant. You document what the function does not necessarily how. So, it takes some object and (potentially) enriches it with more properties keeping the id in place.

/**
 * @typedef Thingamajig 
 * @type {object}
 * @property {number} foo - some number here
 * @property {string} bar - a string used for something
 */

/**
 * Transforms things.
 * @param {string} key - what to transform
 * @return {Promise<Thingamajig>} - transforms into this
 */
function thingDoer(key) { /* .... */ }

/**
 * it does the things
 * @param {Object} input - object containing id and key
 * @param {string} input.id - unique id to associate with return value
 * @param {string} input.key - unique key used to get things
 * @returns {Promise<Thingamajig & {id:string}>} the returned things and id.
 */
const doTheThings = ({id, key}) =>
  thingDoer(key).then(things => {
    ...things,
    id
  })

See on the TypeScript Playground - the file type has been set to JavaScript (found under TS Options), so this is purely a way to demonstrate how the JSDoc will be interpreted. Hover over functions and parameters to see what their types are.

The key thing here is using @typedef to declare the same object structure that thingDoer returns and doTheThings also uses. That way if it changes, you only change one place.

The doTheThings function then returns Thingamajig & {id:string} which is a Thingamajig with an added id property which is a string. An alternative is to create another typedef like ThingamajigWithId and document the property there. Depends on which tool consumes the JSDoc.

/**
 * @typedef ThingamajigWithId
 * @type {object}
 * @property {number} foo - some number here
 * @property {string} bar - a string used for something
 * @property {string} id - unique identifier.
 */

One interesting thing you can do with an intersection (the &) is to declare the type with an ID separately, then intersect the two. This way you can document it:

/**
 * @typedef Thingamajig 
 * @type {object}
 * @property {number} foo - some number here
 * @property {string} bar - a string used for something
 */

/**
 * @typedef WithId
 * @type {object}
 * @property {string} id - unique identifier
 */

/** ...
 * @returns {Promise<Thingamajig & WithId>} the returned things and id.
 */

See on the TypeScript Playground

Note that type intersections are not part of JSDoc itself. They are a TypeScript feature. Depending on which tool consumes the JSDoc it might or might not work. For example, Visual Studio Code likely will not complain, however, if you have a tool that constructs documentation from the annotations, it might not recognise the & as valid.


Also note that there is a long-standing feature to add this type of extension of objects in JSDoc itself:

Issue on GitHub from April 2016
Question on Stack Overflow that spawned it: How to extend a typedef parameter in JSDOC?

Right now there does not seem to be a way to extend a typedef that satisfies all consumers of JSDoc. The only thing that seems to be guaranteed to work is to manually create a typedef which manually repeats all properties (which I showed above):

/**
 * @typedef ThingamajigWithId
 * @type {object}
 * @property {number} foo - some number here
 * @property {string} bar - a string used for something
 * @property {string} id - unique identifier.
 */

However, it does not change with the base typedef for Thingamajig, which makes it annoying to maintain. Here is a brief rundown of different things that might be supported

  • Using an intersection works for anything that bases interpretation on TypeScript.
  • There is also a plugin to extend JSDoc to recognise &.
  • Some tools seem to support using | instead and would show A | B as having both properties from A and B, even though it should be an alternation.
  • Some tools recognise
    /** 
     * @typedef {A, B} Combined 
     */
    
  • Some tools recognise
    /** 
     * @typedef Combined 
     * @type {A, B}
     */
    

I could not find a technique that seems to work consistently everywhere.

VLAZ
  • 26,331
  • 9
  • 49
  • 67