Logo
Logo

Programming by Stealth

A blog and podcast series by Bart Busschots & Allison Sheridan.

PBS 131 of X — Introducing JSDoc

In the previous instalment we focused on the philosophy of documentation — who the audience is, why they need documentation, and what they need from it. We ended the instalment by introducing JSDoc, a tool for documenting JavaScript code.

The previous instalment was all about why we’ll be using JSDoc, this instalment and the one after will focus on how use JSDoc.

Matching Podcast Episode

Listen along to this instalment on episode 708 of the Chit Chat Across the Pond Podcast.

You can also Download the MP3

Episode Resources

Some Fundamental Concepts

Static Code Analysers

First and foremost — JSDoc is a JavaScript static code analyser. That is to say, it parses your JavaScript code and understands it, but doesn’t execute it. A JavaScript interpreter like a web browser or NodeJS parses your code and then executes it, while a static analyser performs the same first step, but then it does something else. Without knowing it, you’ve probably already used many static code analysers.

You may have noticed that all IDEs (integrated development environments) are not equal — some seem positively smarter than others. The less smart ones use regular expressions to do syntax highlighting and little more, but the smart ones perform static code analysis so they can show you where you declared the variable you’ve currently selected, or list all the variables and functions in a side bar for quick access. IDEs that perform static code analysis can also suggest much more useful code completions because they don’t just know that something is a name, but that it’s a function, a variable, a class, etc.

You’ve also had a much more recent encounter with a static code analyser — ESLint! It has to understand your JavaScript to criticise it 😉

Why does it matter that JSDoc is a static code analyser? Firstly, because it means it can automatically discover a lot of information, saving us the need to enter it into the documentation manually. Secondly, because if you have a syntax error in your code, JSDoc will fail.

JSDoc Name Paths

In order to be able to correctly link the various parts of your documentation together, JSDoc has to give every documented aspect of your code a unique name — every variable, function, class, module, etc. must have a unique name. For very basic code that’s easy — everything you define sits in a flat universal namespace, but when you start nesting functions, or using modules and classes that stops being the case.

JSDoc uses three separators when building name paths:

JSDoc also uses :-separated prefixes in the path names for some special elements. For our purposes the most important of these is module: for modules.

Putting it all together, the name path for an instance function named canHaz from a class named LOLCat in a module named memes would be module:memes~LOLCat#canHaz.

The JSDoc Syntax

With JSDoc you write your documentation in your source code, wrapping it in so-called doc comments. Those doc comments must appear directly above the item they document, and items without doc comments are omitted from the documentation.

Doc comments start with zero or more paragraphs of free-form text, and end with zero or more block tags. Both free-form text and block tags can contain inline tags.

Many doc tags expect type expressions, and these are generally surrounded in curly braces.

Doc Comments

In JavaScript, /* starts a multi-line comment, and the comment continues until */. Literally anything can come after that first star and it will be ignored by JavaScript.

Doc comments use this fact to introduce a special kind of comment that’s ignored by JavaScript, but interpreted by JSDoc — the so-called doc comment. Doc comments start with /**, and purely by convention, all lines within the comment are started with a *, they end with a regular */.

/*
 * A regular multi-line comment.
 */
 
 /**
  * A doc comment. That extra star is very important!
  */

Block Tags

The vast majority of JSDoc tags are block tags. Block tags are all pre-fixed with the @ symbol and must be the first significant character on a line. That is to say, the first thing after the indentation and the leading * symbol. Block tags can span multiple lines, continuing until the doc comment ends or a new block tag is started.

/**
 * A function to return the closest thing a computer can to a pancake!
 * @returns {string} Returns a pancake emoji.
 */
 function getPancake(){
     return '🥞';
 }

Think of block tags as being the JSDoc equivalent of <p> or <h1> tags in HTML.

Inline Tags

Inline tags can appear within free text paragraphs, and within the textual parts of block tags. Like block tags, inline tags are named with a pre-fixed @ symbol, but the entire tag, name and all, is enclosed in curly braces.

For now, there is just one inline tag we need to know about {@link}, and we’ll look at it in more detail later, but for now, here’s a simple example to show what they look like:

/**
 * A function to return the closest thing a computer can to a pancake!
 * @returns {string} Returns a pancake emoji.
 * @see {@link https://en.wikipedia.org/wiki/Pancake Wikipedia's page on pancakes} for more information that you probably want 🙂
 */
 function getPancake(){
     return '🥞';
 }

Type Expressions

Type expressions are probably the single most important feature in JSDoc. Other people’s code is like a black box. The only thing developers know is what goes in and what comes out, and type expressions are central to those descriptions.

Type expressions range from the wonderfully simple to the devilishly complex. The good news is that you can build up your level of detail over time as you get more comfortable. You can actually define and name your own type definitions, allowing you to avoid duplicating complex type expressions throughout your documentation.

We’ll build up our understanding as we go, but to illustrate the point, here’s an example of starting simple and improving the expression later:

// A simple first pass

/**
 * The menu as an object. Note that all prices are in Euro.
 * @returns {Array} An array of objects indexed by `name`, `price` , and `description`.
 */
function getMenu(){
    // …
}

// a better second pass

/**
 *The menu as an array of objects. Note that all prices are in Euro.
 * @returns {object[]} Each object is indexed by `name`, `price` , and `description`.
 */
function getMenu(){
    // …
}

// a correct third pass

/**
 *The menu as an array of records. Note that all prices are in Euro.
 * @returns {{name: string, price: number, description: string}[]} Each object is indexed by `name`, `price` , and `description`.
 */
function getMenu(){
    // …
}

// A re-usable custom type definition

/**
 * A menu item.
 * @typedef menuItem
 * @type {object}
 * @property {string} name - the item's name.
 * @property {number} price - the item's price in Euro.
 * @property {string} description - a short sentence describing the item.
 */
 
 /**
 *The menu as an array of items.
 * @returns {menuItem[]}.
 */
function getMenu(){
    // …
}

So, the function returns an array of objects, each of which is indexed by the same three keys, name, price, and description. The name is a string, the price a number, and the description another string.

The first attempt uses the very simple type expression {Array} to simply say that the function returns an array, and then falls back on textual descriptions for the rest of the detail.

The first attempt is overly simplistic to be realistic, but the second attempt is what many people would write, at least initially. The updated type definition {object[]} specifies that the function returns an array of objects, but the descriptions are still capturing all the detail. BTW, this is entirely equivalent to the type expression {Array.<object>}. Both are valid, but some prefer the more explicit longer version, some the shorter version. Use which ever you prefer, but be consistent 🙂

That then takes us to the third version, which captures all the information in the now quite complex type expression {{name: string, pice: number, description: string}[]}. The JSDoc documentation refers to the {key1: valueType1, key1: value2} syntax as specifying a record, but it’s simply a means of specifying the names and types of the keys an object will have, the [] appended to the end simply means an array of such records. Note that this is again entirely equivalent to the more verbose {Array.<{name: string, pice: number, description: string}>}.

The third type expression captures all the information, but, it’s very long, and if your code uses the same type in a few places, you might want to replace it with a name of your choosing. That’s what type definitions are for. The fourth version defines a custom type named menuItem, and then uses it in the function’s type expression {menuItem[]} to specify that the function will return an array of this custom type. Again, this could also be written as {Array.<menuItem>} if you prefer.

Bookmark the Documentation!

There are a lot of JSDoc tags, and while their syntax is generally quite consistent and logical, you’re never going to remember all the subtleties.

It takes a long time to become proficient in complex type expressions, so the most important page to bookmark in the JSDoc documentation is the description of the @type tag which contains a table showing the different syntax options available for use.

Fair warning — the docs look like a throw-back to the 1990s, and some of the entries are a little sparse. However, the alphabetic tag listing is priceless, and clicking on any tag in the list will take you to that tag’s documentation, which almost always includes useful examples.

If you’re going to use JSDoc, bookmark the official website: jsdoc.app.

Learning by Doing — a Worked Illustration

In the instalment zip you’ll find a folder named pbs131a, this is a NodeJS package containing an example module that we’ll be documenting.

Change into this folder and initialise it for use with the following commands:

npm ci
chmod +x main.mjs

The module we’ll be documenting is named Replicator and is defined in the file src/Replicator.class.mjs. This module exports a class representing a Star Trek-like device that can make food appear as if by magic. The module provides a basic menu by default, and the ability to add items to the menu. Each replicator you make from the class has a finite amount of energy, and each item on the menu costs a certain amount of energy to create. Replicators can create food as long as they have sufficient energy, and replicators can be re-charged with new energy.

The file main.mjs imports the module and demonstrate’s it’s use:

#!/usr/bin/env node

// import the class from the module
import Replicator from "./src/Replicator.class.mjs";

// create a replicator
const kitchenFriend = new Replicator();
console.log(`initial charge: ${kitchenFriend.charge}`);

// show the menu
console.log('The Menu:', Replicator.menu);

// make a pancake
console.log(kitchenFriend.replicate('pancakes'));
console.log(`remaining charge: ${kitchenFriend.charge}`);

// add tacos to the menu
Replicator.addMenuItem('taco', '🌮', '42');
console.log('The updated Menu:', Replicator.menu);

// have 2 tacos
console.log(kitchenFriend.replicate('taco', 2));
console.log(`remaining charge: ${kitchenFriend.charge}`);

You can see the code in action by executing main.mjs:

bart-imac2018:pbs131a bart% ./main.mjs
initial charge: 100
The Menu: {
  pancakes: { energyCost: 11, icon: '🥞' },
  popcorn: { energyCost: 1, icon: '🍿' }
}
🥞
remaining charge: 89
The updated Menu: {
  pancakes: { energyCost: 11, icon: '🥞' },
  popcorn: { energyCost: 1, icon: '🍿' },
  taco: { energyCost: 42, icon: '🌮' }
}
🌮🌮
remaining charge: 5
bart-imac2018:pbs131a bart% 

The package.json file specifies JSDoc as a dev dependency, and the file jsdoc.conf.json defines a minimal configuration:

{
    "plugins": ["plugins/markdown"],
    "source": {
        "includePattern": ".+\\.(mjs|jsdoc)$"
    }
}

I was hoping not to need a config file at all for this instalment, but because we need to use the .mjs file extension to keep NodeJS happy, we need to tell JSDoc that it should process files with the .mjs or .jsdoc file extension. Without a customised source.includePattern definition in a config file, JSDoc only processes .js and .jsdoc files, even if you explicitly specify other files in the arguments.

Since I needed a config file anyway, I decided to also enable the optional Markdown support by adding "plugins/markdown" to the plugins array.

The Null Example

Before we look at some appropriate doc comments for the module’s code, let’s see what JSDoc does if you don’t add any doc comments at all!

The correctly documented version of the module is in the file src/Replicator.class.mjs, but there is a second completely undocumented copy of the code in the file named src/Replicator.class.noDocs.mjs. Let’s run JSDoc on this version of the code:

npx jsdoc -c jsdoc.conf.json -d docs-v0 src/Replicator.class.noDocs.mjs

The npx command finds and runs executable files provided by NPM packages, so we’re using it to execute the copy of the jsdoc command that was installed into the local node_modules folder by the npm ci command.

We’re passing Node two flags, -c to specify the config file to use, and -d to specify a destination folder for the documentation. Finally, we’re passing the files to be documented as arguments. In this case, that’s just a single file.

After running this command a new folder will be created named docs-v0, and in there you’ll find the website JSDoc created. Open index.html in your favourite browser, and observe that without doc tags JSDoc produced completely empty documentation.

A nearly empty web page that says Home and that this documentation was generated by JSDoc 3.6.7 along with the date and time.

Generating the True Documentation

Now that we’ve see how little we get for free, let’s generate the real documentation and then look at the doc comments that created it.

npx jsdoc -c jsdoc.conf.json -d docs -R README.md src/Replicator.class.mjs src/*.jsdoc

The command has changed a little from the one we used previously. The -R flag has been added to specify a Markdown file to use for the front page of the documentation, and the destination has been changed to docs. Also, the list of source files has been changed to src/Replicator.class.mjs and all .jsdoc files in the src. There’s just one .jsdoc file, src/typeDefs.jsdoc, and it contains the custom type definitions used in some of the type expressions in the doc comments in src/Replicator.class.mjs.

When you run this command a folder named docs will be created, containing the generated documentation site. Open docs/index.html in your favourite browser to view the docs.

Documenting the Module

All the content making up this final documentation site comes from one of four sources:

  1. The file README.md (just the front page).
  2. The doc comments in the file src/typeDefs.jsdoc.
  3. The doc comments in the file src/Replicator.class.mjs
  4. Inferences JSDoc made using static code analysis

Let’s dive in and have a look at how the code was documented. Keep the generated site open as we go.

When you open the page you’re greeted by a landing page with a description of the thing being documented taking up the majority of the page, and because we’re using the default theme, a menu to the right.

The menu lists the big-ticket items your documentation covers, all the modules, classes, and perhaps a link for the global scope. In this case the list is very sparse since we have one package containing one class, and the global scope.

Let’s start by clicking into the global scope. You’ll see that it describes two custom types that will be used throughout the documentation — one for an amount of charge, and one for a menu item.

Next let’s click on the only module (PBSReplicator). We get a paragraph describing the module, a list of its dependencies, and some relevant external links at the top of the page. Then we get a list of all the package’s classes (just one), followed by detailed descriptions of the one variable (referred to as a member) and the two functions (referred to as methods) the module contains.

The descriptions start with names possibly prefixed with some meta data, and followed by type information for variables, and the return type for functions. Items that are not exported from the module are pre-fixed with the keyword inner to indicate that they can’t be used outside of the module itself. Also notice that the variable is marked as a constant.

The function descriptions describe the arguments (referred to as parameters) they take, the value they return, and any errors they could throw.

Finally, let’s have a look at the class. You’ll see it’s very similar to the module in that it starts with a description, then describes the constructor, and then lists the class’s variables and functions. Again, note the prefixes used to convey metadata like the fact that the menu variable is static.

Adding a Front Page

The front page of this production site is simply the contents of the Markdown file README.md. This wasn’t specified in a doc comment anywhere, it was done with the -R command line flag.

Defining Custom Types

Almost all doc comments refer to a singe definition of some kind, so they appear directly above the relevant piece of JavaScipt. That’s not true of custom type definitions — they don’t exist in JavaScript, they are a purely JSDoc concept, so there’s no obvious place for them to go in the JavaScript files.

If we were to add them into src/Replicator.class.mjs they’d be scoped as being part of the PBSReplicator module, so their name path would become module:PBSReplicator~TypeName, which would be very cumbersome. Instead, we want them in JSDoc’s global name space, and the easiest way to do that is to add them in a separate file. Since that separate file would only contain doc comments and no JavaScript, it wouldn’t make sense to use the .js or .mjs file extensions. Instead, the convention is to use .jsdoc. So, following this convention, I added the doc comments defining two custom types to src/docTypes.jsoc.

The first of these two type defs is very simple:

/**
 * A valid amount of Replicator charge, specifically, a whole number greater than zero.
 * @typedef {number} ChargeAmount
 */

It defines a new type named ChargeAmount that’s a sub-set of the built-in type number.

The second is a little more complex:

/**
 * A replicator menu item.
 * @typedef {object} MenuItem
 * @property {ChargeAmount} energyCost - The amount of replicator charge it
 * costs to replicate one of the item.
 * @property {string} icon - An icon to represent the item, ideally an emoji.
 */

This defines a type named MenuItem that’s a sub-set of the built-in type object, but it takes things a step further by using instances of the @property tag to describe the name and type for each key. Specifically, menu items have a key named energyCost that’s a ChargeAmount as defined in the previous type definition, and a key named icon that’s a string.

Documenting a Module

JSDoc can’t tell that a file defines a JavaScript module without a little help. You need to add a doc comment with an @module tag at the top of the file. Because module definitions support the @requires to specify the modules the module depends on, the most sensible place to put the doc comment is directly above the import statements:

/**
 * Why cook when you can use a replicator! Sadly this module can't provide a real replicator, but it can at least give you a simulated digital one 🙂
 * @module PBSReplicator
 * @requires is_js
 * @see {@link https://is.js.org}
 * @requires lodash-es
 * @see {@link https://lodash.com}
 */
 import is from 'is_js';
import {cloneDeep} from 'lodash-es';

Note the use of the @see tag in conjunction with the inline {@link} tag for pointing to related resources, in this case, the URLs for the imported modules.

Documenting a Variable (the Menu)

When documenting a variable, the most important things to capture as a description of its use, and the type of data it’s intended to contain. The description is simply plain text, and the @type tag is used to document the type.

Our module contains just one variable, menu, and it’s a dictionary mapping food names to menu items:

/**
 * The menu of foods supported by all replicators indexed by name.
 * @type {Object.<string, MenuItem>}
 */
const menu = {
    // …
};

Note the use of the Object.<> syntax for specify the type of the key followed by the type of the values.

Documenting a Function

A function is like a black box — you put something in, some magic happens, and something comes out. So, when documenting a function the most important things to capture are the arguments the function accepts, and the values it promises to return.

/**
 * Test if a given value is a valid replicator charge.
 * @param {*} val - The value to test.
 * @returns {boolean}
 * @see The {@link ChargeAmount} type definition.
 */
function isCharge(val){
    return String(val).match(/^\d+$/) ? true : false;
}

The doc comment above starts with a free-form description, then it uses the @param tag to document the functions only argument, AKA parameter. The parameter tag starts with a type expression followed by the parameter’s name, then optionally a dash followed by a description. Note that the dash acts purely as a separator.

This function checks if a value is a valid charge value, so the parameter can be of any type, which gives me a good opportunity to introduce the special type expression *, which simply means anything.

The @returns tag is used to describe the function’s output. It starts with a type expression and is optionally followed by a description.

Finally, notice that we can use JSDoc name paths in {@link} tags as well as traditional URLs. Links of this form will jump the user to the relevant section of the documentation.

Loosely speaking, functions can output something other than their return value — they can throw an exception. Your functions should document each type of exception they can throw with separate @throws tags:

/**
 * Assert that a given value is a valid replicator charge by testing it, and
 * throwing an error if it isn't.
 * @param {*} val - The value to assert.
 * @returns {number} Returns the test value forced to be a number.
 * @throws {TypeError} Throws a Type Error if the asserted value is not valid.
 * @see Data validated by the {@link module:PBSReplicator~isCharge isCharge()} function.
 */
function assertCharge(val){
    if(!isCharge(val)) throw new TypeError('invalid charge, must be an integer greater than 0');
    return parseInt(val);
}

Notice the use of a full JSDoc name path in the {@link} inline tag in the @see block tag. Also, notice that if you place any text after the URL or name path but still within the {@link}, that text becomes the link text.

Documenting a Class

For a class to be included in the finished documentation you must add a doc comment directly above it. The comment doesn’t need to contain any tags, just a description is fine.

Within the class, getters/setters should be documented like variables, because that’s how they appear to users of the code. That means you don’t use @returns or @param, but @type instead:

/**
 * The menu shared by all replicators as a dictionary of named menu items.
 * @type {Object.<string, MenuItem>}
 */
static get menu(){
    return cloneDeep(menu);
}

The constructor on the other hand should be documented like a function, but without an @returns tag:

/**
 * Replicators default to an initial charge of 100, but an alternative
 * initial charge can be passed.
 * @param {ChargeAmount} [initialCharge=100] - The replicator's initial charge.
 */
constructor(initialCharge){
    this._charge = 0;
    if(is.not.undefined(initialCharge)){
        this.recharge(initialCharge);
    }else{
        this.recharge(100);
    }
}

This constructor, like many, supports an argument, but it’s optional. In JSDoc, an argument is marked as optional by surrounding it’s name within the @param tag with square brackets. Optional arguments very often have a default value, that can be added to the documentation by adding an = symbol after the name followed by the default value. Note that this all goes within the square brackets. In the above example, the constructor accepts one optional argument named initialCharge of type ChargeAmount, and if not passed, it will default to 100.

Finally, functions within classes are documented like regular functions:

/**
 * Make some food from the menu.
 * @param {string} item - The name of the item to make.
 * @param {number} [num=1] - The amount of the item to make. Note that
 * nonsense values are ignored.
 * @returns {string} The icons representing the created food.
 * @throws {RangeError} A Range Error is thrown if the named food doesn't
 * exist on the menu, or if making the food would consume more energy than
 * is available.
 * @see #charge
 * @see #recharge
 */
replicate(item, num){
    // …
}

I chose this function as an opportunity to draw your attention to something else — the @see tag can accept a JSDoc name path as its only value, and it will render that as a link to the relevant part of the documentation. Notice that within classes you can use relative name paths, the full name path to the charge instance property would be module:PBSReplicator~Replicator#charge, but because the @see tag is inside the class, you can use #charge to refer to ‘the instance property/function in this class named charge.

General Tip — Good Names Result in Shorter Doc Comments

JSDoc rewards you for taking the time to choose good variable, function, class, argument, and type definition names because they reduce the need for descriptions.

To illustrate the point — which of the following is clearer?

/**
 * @param {ChargeAmount} [i=100] - The replicator's initial charge.
 */
 
 // or
 
/**
 * @param {ChargeAmount} [initialCharge=100]
 */ 

They’re at least equivalent in clarity, but the second option is shorter to write, more concise for readers, and at least as clear, if not clearer to the reader.

While good code isn’t quite self documenting like the cliché would have us believe, it does need a lot less effort to document clearly 🙂

Final Thoughts

Join the Community

Find us in the PBS channel on the Podfeet Slack.

Podfeet Slack