First and most importantly, I want to congratulate you.
Wait, What?
Yes, you heard it right. Congratulations! Writing proper documentation helps you get one step closer to writing clean code. If you don’t believe me, you might want to read the book Clean Coder from “Uncle” Bob. Trust me, it is an excellent investment in your programming career. I wrote an article sharing the major takeaways from reading the Clean Coder.
Enough talking and straight to the topic.
I am a firm believer in the following statement: high quality written code doesn’t need to be documented.
The reason why is because the code should be written in a way that is easy to read.
It’s like reading any of the dummies books, they are written with the purpose of making the learning process easy.
In a similar way, code needs to be written for dummies.
Are you calling me a dummy?
No, but you deserve one for that question dummy.
Write code that is easy to read. However, I know what you are going to tell me…
Even after writing quality code, it can take time to understand how everything works for someone new to the codebase.
In this case, I have to acknowledge you are right. In my years of experience in the software development industry, I must admit documenting your code is not a luxury, but a necessity.
I love writing code using JavaScript. It allows me to develop the front-end and the back-end of an application with the same programming language.
JavaScript is known for its high flexibility, and it is because of that flexibility that too many problems can arise if you are not experienced enough with the programming language. Among the several issues found is the lack of data types defined in functions or variables.
Yes, TypeScript has been the solution for this never-ending JavaScript problem. However, sometimes you are in projects in which it is impossible to start all the way from zero to write your code in TypeScript rather than JavaScript. This is mostly found in Node.js + Express.js APIs as well as React.js apps.
Writing proper documentation in JavaScript does take your code to the next level.
I mean it.
You can write JavaScript documentation using JSDoc. Please, allow me to be your instructor for today.
Table of Contents
What is JSDoc?
According to Wikipedia, JSDoc is a markup language used to annotate JavaScript source code files.
If this doesn’t make sense, I will explain it with my own words: JSDoc allows you to add comments to document your JavaScript code.
Configure Visual Studio Code
Before we start learning how to implement JSDocs in our JavaScript code, we are going to modify the configuration in Visual Studio Code.
In Visual Studio Code, go to Preferences > Settings. Then, add the following configuration:
"javascript.implicitProjectConfig.checkJs": true
Once this process is completed, you will start noticing Visual Studio Code is acting as TypeScript. Feel free to change this configuration back to its default value if you don’t like it after completing this tutorial.
Learn By Example
One of the most effective ways to learn is by example, and that’s exactly what we are going to do in this simple, yet important tutorial.
Simplest form of Documentation by providing a description
Let’s start with a simple function. In this case, you will see the getCities function.
function getCities() { return [ 'Barranquilla', 'San Francisco', 'Austin', 'Miami', 'Chicago', 'New York', 'London', 'Bogota', 'Buenos Aires', 'Madrid' ]; }
Let’s analyze this for a second.
Based on the function’s name, we can infer we are going to expect to get cities. For many of you, this doesn’t need documentation.
However, depending on how serious you are about writing documentation, you would add comments prior to your function.
/** * gets a list of cities from all over the world */ function getCities() { return [ 'Barranquilla', 'San Francisco', 'Austin', 'Miami', 'Chicago', 'New York', 'London', 'Bogota', 'Buenos Aires', 'Madrid' ]; }
Now, we are using JSDocs. Generally, we provide a brief description of what the function does.
Sometimes you will find yourself feeling like explaining something that is already self-explanatory. However, you will get good at writing simple, yet meaningful descriptive comments.
Although this is a start, let me tell you that you became better than most average JavaScript programmers.
I’m serious.
Let’s keep learning more.
Using JSDoc tags to describe your code
JSDoc provides special tags that can be used to provide more information related to your code. Using our previous example, we could use the tag @description to mention that the comment next to the tag represents a description.
/** * @description gets a list of cities from all over the world */ function getCities() { return [ 'Barranquilla', 'San Francisco', 'Austin', 'Miami', 'Chicago', 'New York', 'London', 'Bogota', 'Buenos Aires', 'Madrid' ]; }
There is a longer list of special tags you could use with JSDocs, which you can find in their website.
Most Commonly used JSDoc Tags
In this section we are going to cover the special tags that are used over and over to document JavaScript code.
@return or @returns tag
Let’s make some changes to our getCities function. After all, our code can be changed at any point. In this case, I’m going to convert the function to return the countries associated with the cities.
/** * @description gets a list of location objects with cities from all over the world */ function getLocations() { return [ { country: "Colombia", city: "Barranquilla" }, { country: "USA", city: "San Francisco" }, { country: "USA", city: "Austin" }, { country: "England", city: "London" }, { country: "Argentina", city: "Buenos Aires" }, { country: "Spain", city: "Madrid" }, ]; };
If you noticed, our function is no longer called getCities, and now it is called getLocations. We updated the result and the documentation if you didn’t notice.
If you are using Visual Studio Code as your IDE of choice, you should hover over the function, and you will notice the description of the function as well as the object structure of the result.
You could also specify what kind of object the function returns using the @return or @returns tag.
/** * @description gets a list of location objects with cities from all over the world * * @returns {{country: string, city: string}[]} - locations */ function getLocations() { return [ { country: "Colombia", city: "Barranquilla" }, { country: "USA", city: "San Francisco" }, { country: "USA", city: "Austin" }, { country: "England", city: "London" }, { country: "Argentina", city: "Buenos Aires" }, { country: "Spain", city: "Madrid" }, ]; };
To specify the object type returned using the @returns tag, you will have to wrap the data type inside curly braces. You can see other examples below.
/** * @returns {string} country record */ function getCountry() { return 'Colombia'; } /** * * @returns {string[]} list of countries */ function getCountries() { return ['Colombia', 'USA', 'England', 'Argentina', 'Spain']; } /** * * @returns {number[]} list of country codes */ function getCountryCodes() { return [57, 1, 44, 54, 34]; } /** * @returns {{country: string, city: string}} - location recorded */ function getLocation() { return { country: "Colombia", city: "Barranquilla" }; } /** * @returns {Date} the current date */ function now() { return new Date(); }
If you noticed, I’ve used different data types such as string, array of strings, array of numbers, date, and even custom objects.
Are you starting to feel like you are using Typescript? In my opinion, I am.
@param tag
The likelihood of us writing functions without parameters is slim, and parameters are part of the codebase that needs to be documented.
Let’s look at a simple example of how we use the @param tag. That’s why we are going to make minor changes to our getLocations function to add the ability to filter the locations based on the country provided.
/** * @description gets a list of location objects with cities from all over the world * * @param {string} country - provide the country of the cities to fetch * * @returns {{country: string, city: string}[]} locations */ function getLocations(country) { return [ { country: "Colombia", city: "Barranquilla" }, { country: "USA", city: "San Francisco" }, { country: "USA", city: "Austin" }, { country: "England", city: "London" }, { country: "Argentina", city: "Buenos Aires" }, { country: "Spain", city: "Madrid" }, ].filter((location) => location.country === country); }
Now, let’s attempt to call the function using VSCode and let’s inspect the results.
Did you see what happened?
Visual Studio Code Intellisense to the rescue!
In this case, Intellisense is providing us with meaningful information about the parameter data type we need to provide. In this example, we can infer the parameter country will be a data type of string.
However, I will make changes to the function again and include the parameter id.
/** * @description gets a list of location objects with cities from all over the world * * @returns {{country: string, city: string}[]} locations */ function getLocations(id) { return [ { id: 1, country: "Colombia", city: "Barranquilla" }, { id: 2, country: "USA", city: "San Francisco" }, { id: 3, country: "USA", city: "Austin" }, { id: 4, country: "England", city: "London" }, { id: 5, country: "Argentina", city: "Buenos Aires" }, { id: 6, country: "Spain", city: "Madrid" }, ].filter((location) => location.id === id); }
What do you think the value of id should be? A string? A number?
How many times have you jumped into a new project and read the code for a while to know the kind of data type of the id parameter?
Even worse, have you had to check the database table to understand the data types as you still can’t easily figure out the id data type after inspecting the code?
Making a small change like adding the @param tag to the documentation might initially seem irrelevant, but in the long run other programmers will thank you.
/* @param {string} id - provide the id of the location to fetch
After making our changes, we get an understandable description of what the function does, and we no longer need to guess what data types each parameter of our function has.
This is much better and makes our JavaScript development much smoother.
What are the data types supported by JSDoc?
In the previous examples, you noticed the usage of some data types. However, in case you are not familiar with the available options, the following are data types that can be used with JSDocs:
- Null
- Undefined
- Boolean
- Number
- String
- Object
- Array
- Set
- Date
- Void (Used if your function doesn’t return anything)
- Function
All these data types are built-in JavaScript types.
Using Custom Data Types
As you know JavaScript provides tons of flexibility when it comes to generating, i.e., custom Objects or Arrays. This can be either a benefit or the root of messy code depending on the opinion you have about JavaScript.
Nevertheless, we are faced with custom data types day in day out. Fortunately, we can define custom data types with JSDoc.
Defining the Type Definition using @typedef and @property tags
In getLocations function example, you saw we were returning an array of location objects. Therefore, we defined the object returned the following way:
/* @returns {{id: number, country: string, city: string}[]} locations
This is perfectly fine. However, if this location object is used over and over for several processes in our code, it will turn into a bulkier documentation. Also, in case we decide to return more values related to a LocationOption object, i.e. longitude and latitude, we will have to update the object structure everywhere we have decided to use a location object in our code…
… and that’s not fun.
Or, what usually happens, you will forget to update the documentation everywhere else.
Therefore, we are going to define our custom data type object using @typedef and @property tags like in the following example:
/** * @typedef LocationOption * * @property {number} id * @property {string} country * @property {string} city */
Having defined our LocationOption data type, we can now update our getLocations function’s documentation…
/** * @description gets a list of location objects with cities from all over the world * * @param {number} id provide the id of the location to fetch * * @returns {LocationOption[]} locations */ function getLocations(id) { return [ { id: 1, country: "Colombia", city: "Barranquilla" }, { id: 2, country: "USA", city: "San Francisco" }, { id: 3, country: "USA", city: "Austin" }, { id: 4, country: "England", city: "London" }, { id: 5, country: "Argentina", city: "Buenos Aires" }, { id: 6, country: "Spain", city: "Madrid" }, ].filter((location) => location.id === id); }
…and the best of all this is when you are leveraging Visual Studio Code to your own benefit to understand the correct data type of a variable:
I feel I’m in TypeScript land without using it, ditching the compilers required to convert your TypeScript code to JavaScript.
Using Custom Data Types in multiple files
Let’s be honest, our applications are often composed by multiple files. Multiple files representing, i.e., in the case of APIs, controllers, services, routes, queries, middlewares, etc.
Let’s say our custom LocationOption data type will be used in different services. If you want to use the custom data type on your documentation, you will need to define the same data type on each service file.
“Nope, I ain’t doing that!”
Don’t worry, I hear you on that.
Lately, I’ve been using the following approach to solve this is by defining a myService.typedefinitions.js file. There I define the custom data type definitions.
If you configured your Visual Studio Code to enable semantic check for JavaScript file, the IDE should recognize in all your files the existence of the custom data types.
However, if you didn’t set this configuration, you will need to import myService.typedefinitions.js files to your JavaScript files.
require('./example.typedefinitions');
Another approach for this is to set up a @types/myProject folder where you will generate all the files containing all the type definitions used on your project, i.e.:
- @types/myProject
- car.typedefinitions.js or car.d.js
- person.typedefinitions.js or person.d.js
- location.typedefinitions.js or location.d.js
Using Custom Data Types Without Defining the Data Type
There are different circumstances when you don’t want to set a custom data type as it could be a one case scenario where you will see this custom data type.
One example of this is when configuring an API using Node.js + Express.js. If you have used Express.js, you know you can set a simple endpoint with the route of /locations:
const express = require('express'); const app = express(); const port = 3000; app.get('/locations', getLocations)); app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`) });
Once again, we need to make small changes to our getLocations function to accept the Express request and response as parameters. We will still provide an id to filter locations based on the id query parameter.
Finally, we will send the results as part of the response body.
/** * @description gets a list of location objects with cities from all over the world * */ function getLocations(req, res) { res.send( [ { id: 1, country: "Colombia", city: "Barranquilla" }, { id: 2, country: "USA", city: "San Francisco" }, { id: 3, country: "USA", city: "Austin" }, { id: 4, country: "England", city: "London" }, { id: 5, country: "Argentina", city: "Buenos Aires" }, { id: 6, country: "Spain", city: "Madrid" }, ].filter((location) => location.id === req.query.id) ); }
However, who else is going to know the parameter req which is an object containing a query property and is also an object containing our id?
We can help by setting our custom object while defining the parameters accepted in the function like the following:
/* @description gets a list of location objects with cities from all over the world * * @param {Object} req Express Request * @param {Object} req.query Query Parameters * @param {number} req.query.id provide the id of the location to fetch * @param {Object} res Express Response * @param {Function} res.send Express function to send response in the body * * @returns {void} */ function getLocations(req, res) { res.send( [ { id: 1, country: "Colombia", city: "Barranquilla" }, { id: 2, country: "USA", city: "San Francisco" }, { id: 3, country: "USA", city: "Austin" }, { id: 4, country: "England", city: "London" }, { id: 5, country: "Argentina", city: "Buenos Aires" }, { id: 6, country: "Spain", city: "Madrid" }, ].filter((location) => location.id === req.query.id) ); }
Look at how we are defining the req Object in our documentation. What is happening there is:
- Req: the req parameter is an object.
- Req.query: the req object contains a query property which is an object.
- Req.query.id: the query object contains an id property which is a number.
Other things to notice:
- The res parameter is also an object which contains a send function property.
- The getLocations function doesn’t return anything. Therefore, the @returns got updated with a void.
NOTE: If you are using express, you could define Express.Request or Express.Response as valid data types for your req and res parameters.
Resources
Interested in digging up more information about JSDocs? You can find helpful information in the following links:
Funny Fact
Recently, one coworker sent me a message to Slack saying thanks.
I was like: You’re welcome! But… What did I do?
He was going through some code I had written a while ago on one of the microservices I worked on probably about year ago.
The reason he said thanks was because I had left plenty of comments throughout the code. It was the first time I had received a comment like that.
However, programmers silently thank other programmers who previously worked on the same project for making it easy to read.
Going the little extra mile to write good comments almost instantly allows you to stand out among many programmers in this world.
Conclusions
- As professional programmers, we are on the mission of making easy the process of reading the code not just for ourselves, but for future generations of programmers.
- It is a lifesaver. After two months, two weeks, or even two days you will forget what piece of code you wrote. Decreasing the time spent to understand the code is critical when the code needs modifications, especially whenever there is a production failure and there is not much time for guessing.
- It removes any ambiguity caused by the flexibility of JavaScript. Never again will you have to guess whether an id parameter or property is a number or string.
- It brings joy to programming in JavaScript without the need of using TypeScript, especially in projects that cannot be started in TypeScript as there is major development progress made on JavaScript.