Documenting a callback function in JSDoc

A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.

-- MDN: Callback function

Callbacks are used all over in JavaScript, a good example is the Array.prototype.some(callbackFn[, thisArg]) function:

[1, 2].some(function (kValue, k, O) {
    console.log(`Key ${k} = ${kValue} in [${O.join(',')}]`);
    return true;
});
// Outputs:
// Key 0 = 1 in [1,2]

This callback function has multiple arguments and a return value, VSCode does a great job of documenting the function via the IntelliSense tooltip:

MDN also has this information covered in the Parameters section of the Array.prototype.some() information page.

The endgame is to achieve the same documentation on custom functions as native functions. Meaning I would like to get tooltip information inside my IDE (VSCode) and JSDoc output to document callback functions.

JSDoc output isn't always important, but if you're using it to create documentation, for a library or something else, it needs to be able to sufficiently document a callback.

The setup

So the very simple setup looks like this:

function mathFn(in1, in2, callbackFn) {
  return callbackFn(in1, in2);
}

function sumCallback(in1, in2) {
  return in1 + in2;
}

mathFn(1, 2, sumCallback); // Returns 3.

The mathFn (outer function) takes 2 numbers and a callback. In this example the sumCallback (callback function) is a simple function that sums the 2 numbers.

Take 1: Letting VSCode document the functions

Using VSCode's [Quick Fix - Infer parameter types from usage] feature to add docblocks to the 2 functions:

/**
 * @param {number} in1
 * @param {number} in2
 * @param {{ (in1: any, in2: any): any; (arg0: any, arg1: any): any; }} callbackFn
 */
function mathFn(in1, in2, callbackFn) {
  return callbackFn(in1, in2);
}

/**
 * @param {any} in1
 * @param {any} in2
 */
function sumCallback(in1, in2) {
  return in1 + in2;
}

Which provided a nice boilerplate, I've modified the output a bit, added return value and some descriptions, so the final result looks like this:

/**
 * @param {number} in1
 *   First number
 * @param {number} in2
 *   Second number
 * @param {{ (in1: number, in2: number): number }} callbackFn
 *   Computation callback
 * @returns {number}
 *   The result
 */
function mathFn(in1, in2, callbackFn) {
  return callbackFn(in1, in2);
}

/**
 * @param {number} in1
 * @param {number} in2
 * @returns {number}
 */
function sumCallback(in1, in2) {
  return in1 + in2;
}

Result

This gives the perfect tooltip, with every type hinting inside the callback documented. It even suggests parameter naming of the callback function:

Exactly what I was looking for, except JSDoc wont parse this, returning with the following error message:
ERROR: Unable to parse a tag's type expression for source file

So if I didn't need JSDoc to create documentation this would be my go to method. But it would be nice to have JSDoc working, so the quest continues.

Take 2: Documenting the callback inline

JSDoc supports a function type, so I've changed the type definition to look like this:

/**
 * @param {number} in1
 *   First number
 * @param {number} in2
 *   Second number
 * @param { function(number, number): number } callbackFn
 *   Computation callback
 * @return {number}
 *   The result
 */
function mathFn(in1, in2, callbackFn) {
  return callbackFn(in1, in2);
}

Result

VSCode still does a good tooltip job. Note the callback arguments are no longer named, but simply called arg0 and arg1.

Having the named arguments would be great, it gives a great hint at what to expect from the arguments in a callback. Unfortunately it's not supported, so the experience is a bit worse than take 1, but JSDoc parses everything and produces an output:

So far so good. Problem is the callback is not documented, it simply states it's a function, but we got no information about the arguments. Looking through the JSDoc issues I'm not the only one encountering this problem: Ability to document callback.

The reason for this, is that JSDoc did not parse the @param type to anything other than a function, using the --explain option we can see JSDoc data structure, in this case the important part of the explain dump is this:

{
    "type": {
        "names": [
            "function"
        ]
    },
    "description": "<p>Computation callback</p>",
    "name": "callbackFn"
}

Take 2.5: Inline with added description

It's possible to add information about the callback arguments as simple text in the parameter description:

/**
 * @param {number} in1
 *   First number
 * @param {number} in2
 *   Second number
 * @param { function(number, number): number } callbackFn
 *   Computation callback
 *
 *   **Callback arguments**
 *   1. in1: *number* \
 *   First number
 *   2. in2: *number* \
 *   Second number
 *
 * @return {number}
 *   The result
 */
function mathFn(in1, in2, callbackFn) {
  return callbackFn(in1, in2);
}

Result

The tooltip:

And the JSDoc output:

Both JSDoc and VSCode support markdown, so this extra description can be formatted pretty much in anyway you like. The problem with this is, you can format it anyway you like, so it's up to the developer to always do this the same way to keep consistency in the output. It also makes it impossible to change layout of the argument documentation later on by changing the JSDoc theme.

Take 3: Defining a callback

Using the @callback tag to define a callback, and using this defined type in the function docblock:

/**
 * Callback short description
 *
 * Long description
 *
 * @callback callbackFn
 *
 * @param {number} in1
 *   The first number in the computation
 * @param {number} in2
 *   Second number in the computation
 * @returns {number}
 *   Result
 */

/**
 * @param {number} in1
 *   First number
 * @param {number} in2
 *   Second number
 * @param {callbackFn} callbackFn
 *   Computation callback
 * @return {number}
 *   The result
 */
function mathFn(in1, in2, callbackFn) {
  return callbackFn(in1, in2);
}

Result

Unfortunately this broke the initial tooltip:

VSCode doesn't expand the definition, instead it just says the callbackFn parameter is of the callbackFn type.
However when you start typing out an anonymous function inside the mathFn function call VSCode will jump in and tell you the parameters, including parameter namings of the callback:

So it's not all bad, it could be better.
But the JSDoc output finally provided information about the parameters (and return value) of the callback function. Although it's "hidden" behind a link to the callbackFn type (much like VSCode) it's still a great improvement. So the parameter list looks like this now:

And the callbackFn definition looks like this:

Creating a callback definition also allows you to reuse the same callback documentation for multiple places.
Agreed that most of the time a callback is tied to the outer function, but it's a tiny plus for when this is not the case.

Type checking

VSCode also allows type checking Javascript, and thankfully all the above methods works with this, so the IDE can give you warnings, I've added an invalidCallback() function that takes a string as the first argument instead of a number:

/**
 * @param {string} in1
 * @param {number} in2
 * @returns {number}
 */
function invalidCallback(in1, in2) {
  return 0;
}

VSCode will highlights this:

In what feels like serendipity, I just read a recent post by Thomas Portelange, that gives an introduction to this exact feature: Typescript... or jsdocs ?

Conclusion

Unfortunately I haven't found the perfect solution for documenting a callback in both VSCode and JSDoc.

The 3 methods I've tested here all have pros and cons, so your use case will determine what kind method you'll choose. I would go with the following:

  • Take 1: If you do not need JSDoc go with this. I think getting a naming suggestion for your parameters is a huge plus, and a great named parameter will go a long way in understanding what parameters are available in a callback.

  • Take 2: If you don't mind writing a bit of extra boilerplate in the comment.
    When working in VSCode you'll miss out on the parameter naming, but an added description to the parameter can make up for this (given it's subjectively harder to read).
    The good thing about adding the description is it'll enrich the JSDoc presentation as well.

  • Take 3: Would be my goto for writing libraries that needed a strong documentation outside of the IDE.
    Even though it doesn't add parameter naming in the first tooltip, it does when adding an anonymous function, and adding a TypeScript declaration file (.d.ts) could make up for it inside VSCode (although there's extra work involved).

Future improvements

My favorite approach is the first (Take 1), it's the easiest to write and gives excellent tooltips. But looking at the JSDoc source code I'm not sure it's possible to get it working without hacking (or forking) it.

I'm still deep diving into the JSDoc tool trying to figure out better ways for documenting a callback. I'll make another post about improvements to callback documentation, particularly in JSDoc when I'm done testing.
I'll touch down on my thoughts of implementing Take 1 in JSDoc, and an easy trick to improving the documentation output of Take 3.

Did you find this article valuable?

Support Philip Birk-Jensen by becoming a sponsor. Any amount is appreciated!