JSDoc: a solid alternative to TypeScript
Many developers prefer using TypeScript because of its type-checking functionalities. However, this requires an extra transpiling step, which can be a bother and a time waste. This article will show you how to use JSDoc to get all the same type of controls but write plain JavaScript for the fastest development time and better documentation as well!
Discover how at OpenReplay.com.
JavaScript has cemented its place as one of the most used scripting languages in recent times. It is known for its ease of writing scripts on the web platform. As the language evolved, it went from being a ‘toy’ language capitalizing on the success of Java to being a full-blown language used to build more than just small scripts.
Unfortunately, this sheds light on the flaws of the language. Some of these include;
-
Lack of Static Typing and Strict Type Checking: Javascript is all forgiving where arguments could be passed into functions that don’t accept it, required values could be omitted, etc. This is not the case for statically typed languages as it would error on compile time. These errors creep into production in Javascript applications.
-
Difficulty in Scaling and Maintaining Large Codebases: JavaScript doesn’t provide strong mechanisms for managing large codebases, making it challenging to scale and maintain projects over time.
Typescript: The Hero
In 2014, Microsoft launched Typescript v1.0. This changed the entire JavaScript Ecosystem.
TypeScript, which is a superset of JavaScript, solved those problems stated above and more. This made it increasingly popular even in recent times.
State of Js survey 2022 showing TypeScript usage rise.
TypeScript, while solving a lot of problems, did not come without its downsides.
In this article, we will look at a very good alternative to TypeScript called JSDoc, which solves the problems of static typing and scalability while also eliminating those downsides of TypeScript for the JavaScript ecosystem.
What is JSDoc
JSDoc is a documentation system for JavaScript that helps a JavaScript code. It works by using comments containing the JSDoc syntax.
The JSDoc syntax serves multiple purposes, including annotating values with types, specifying parameter and return types for functions, documenting and providing usage information for functions, typing errors, etc. These could then be utilized, similarly to TypeScript, by code editors to serve as a guide for programmers building, consuming, or maintaining the said codebase.
JSDoc vs Typescript
JSDoc and TypeScript both solve the problems with writing and maintaining plain JavaScript code. However, they used different approaches, which have their benefits and drawbacks.
Benefits of JSDoc Over Typescript:
-
Flexibility and Compatability: JSDoc being just JavaScript comments, means it could be added to any JavaScript codebase regardless of the language version, and it is not tied to a compiler like TypeScript is.
-
Code Annotation: JSDoc could be used for more than just type checking. It could be used to add more documentation, describe how functions work, and generate a documentation website, all providing value to enhance code maintainability and understanding.
-
No Compilation Step: This is one of the most motivating reasons to switch to JSDoc coming from TypeScript. TypeScript requires compilation to change the Typescript code into Javascript so the browser can understand it, while JSDoc does not require any other step as they are just ‘comments’, which is a supported feature of Javascript itself. This can simplify and increase the speed of development workflow compared to using the necessary Typescript build pipeline each time you make changes.
Drawbacks Of Using JSDoc
While JSDoc has a lot of advantages over TypeScript, there are reasons Typescript usage keeps being increasingly adopted over time. These are some of the advantages of Typescript over JSDoc:
-
Stronger Static Typing: TypeScript provides a strong model for types and catches these errors at compile time. Unlike JSDoc, where these typing end in the code itself and isn’t enforced
-
Type Inference: TypeScript can infer the type from its value. This helps reduce explicit type annotations and makes the codebase less verbose.
-
Transpilation: TypeScript can adopt the latest and future features of the JavaScript language with its polyfill feature. It effectively transpiles these codes down to understandable versions for browsers that would not have support for the features yet.
How To Use JSDoc: The Basics
Due to its longevity, JSDoc has extensive support in all modern editors and can be used out of the box without any installation.
Adding JSDoc in a .js
file, as stated to be just comments, is done by opening a comment with an extra *
// Normal Javascript Comment 1
/* Normal Javascript Comment 2 */
/**
JSDoc containing two asterisks
*/
These are some of the basic features to get started.
Adding Code Description To A Block of code:
/** The name of the language JSDoc is written for*/
const language = "JavaScript"
Adding Types To Values:
/**
* This represents the writer of this blog
* @type {string}
*/
const writerName = "Elijah"
Now, it is denoted that the username variable should be of type string.
Adding Types To Objects And Arrays:
/**
* @type {Array<string>}
*/
const colours = ['red', 'blue', 'green']
/**
* @type {number[]}
*/
const primeNumbers = [1, 2, 3, 5, 7]
Both methods are valid in JSDoc (same as in Typescript).
Creating an object type could be achieved by using the @typedef
directive.
/**
* * @typedef {Object} User - A user schema
* @property {number} id
* @property {string} username
* @property {string} email
* @property {Array<number>} postLikes
* @property {string[]} friends
*/
/**@type {User} */
const person1 = {
id: 847,
username: "Elijah",
email: "elijah@user.com",
postLikes: [44, 22, 24, 39],
friends: ['fede', 'Elijah']
}
/** @type {User} */
const person2 = {
id: 424,
username: "Winston",
email: "winston@user.com",
postLike: [18, 53, 98],
friends: ['Favour', 'Jane']
}
Typing Functions (parameters, return, and expected error types):
/**
* Divide two numbers.
* @param {number} dividend - The number to be divided.
* @param {number} divisor - The number to divide by.
* @returns {number} The result of the division.
*/
function divideNumbers(dividend, divisor) {
return dividend/divisor;
}
The @param
keyword, followed by defining a type, represents the value the defined function would accept. You could also add some description of what the parameter is beside it after a hyphen (-).
The @returns
keyword is used to define what is returned by the function. This is especially useful for large functions. It may be difficult to go through all the code, including early returns, to determine what is expected of the function.
Additionally, you could add possible errors the function could throw using the @throws
directive.
Improving the division function, we could specify that it returns an error if the divisor is zero as well as handle that in the code itself.
/**
* Divide two numbers.
* @param {number} dividend - The number to be divided.
* @param {number} divisor - The number to divide by.
* @returns {number} The result of the division.
* @throws {ZeroDivisionError} Argument divisor must be non-zero
*/
function divideNumbers(dividend, divisor) {
if (divisor === 0) {
throw new DivisionByZeroError('Cannot Divide by zero')
}
return dividend/divisor;
}
@throws
could take either an error type (ZeroDivisionError) or a description (Argument divisor must…) or both.
/**
* Custom error for division by zero.
*/
class DivisionByZeroError extends Error {
constructor(message = "Cannot Divide By Zero") {
super(message);
this.name = "DivisionByZeroError";
}
}
Since JavaScript does not natively force you to handle errors, you must do so, as it helps improve collaboration and maintenance.
Typing Full Classes (description, constructor, and methods)
Going a step further, you could also type a full class syntax with JSDoc.
/**
* A Rectangle Class
* @class
* @classdec A four-sided polygon with opposite sides of equal length and four right angles
*/
class Rectangle {
/**
* Initializing a Rectangle object.
* @param {number} length - The length of the rectangle.
* @param {number} width - The width of the rectangle.
*/
constructor(length, width) {
this.length = length;
this.width = width;
}
/**
* Calculate the area of the rectangle.
* @returns {number} The area of the rectangle.
*/
calculateArea() {
return this.length * this.width;
}
/**
* Calculate the perimeter of the rectangle.
* @returns {number} The perimeter of the rectangle.
*/
calculatePerimeter() {
return 2 * (this.length + this.width);
}
}
Above is a simple rectangle class with two methods to calculate its area and perimeter.
The @class
keyword is used to show that a function is needed to be called with the new
keyword.
@classdec
is used to describe the entire class.
When typing classes, it is important to go further by adding types and descriptions to
- The constructor
- All methods and variables created within the class
We used the @params
keyword to provide the types and descriptions of the arguments that need to be passed into the constructor function.
Methods in the class are typed the same way as functions, which was covered in the previous section.
Improving General Code Documentation:
Aside from adding essential types to the code, there are a lot of ways JSDoc helps improve readability and ease of understanding. Here are a few of them:
- Adding Code Authors: The author of an item could be added using the
@author
directive with the name and email of the author
/**
* Possible title for this article
* @type {string}
* @author Elijah [elijah@example.com]
*/
const articleTitle = "Demystifying JSDoc"
- Example Usage: You could also add code snippets showing how a particular code block should be used. This is especially useful for complex blocks of code.
/**
* Sums of the square of two numbers a**2 + b**2
* @example <caption>How to use the sumSquares function</caption>
* // returns 13
* sumSquares(2, 3)
* @example
* // returns 41
* sumSquares(4, 5)
* // Typing the function
* @param {number} a - The first number
* @param {number} b - The second number
* @returns {Number} Returns the sum of the squares
* */
const sumSquares = function(a, b){
return a**2 + b**2
}
We use the @example
directive to achieve this. This can also be captioned with the caption
tag.
- Versioning: You could also specify the version of an item using the
@version
directive.
/**
* @version 1.0.0
* @type {number}
* */
const meaningOfLife = 42
- Helpful Links: Often, you might want to point the user to somewhere else they could gain more knowledge about the code. It could be a GitHub repo, some tutorial, blog, etc. To do this, two directives help achieve it.
@link
and@tutorial
.
/**
* How to use the link tags
* Also see the {@link https://jsdoc.app/tags-inline-link.html official docs} for more information
* @tutorial getting-started
* */
function myFunction (){
}
The @link
tag renders official docs
as a link to the specified link. It is used to create a link to the URL you specify, while the @tutorial
tag is used to direct the user to a relative tutorial link on the generated docs.
- Creating Modules: Creating a module in JSDoc can be done using the
@module
tag at the top of the file. This makes the current file a module. Modules are grouped in a separate section on the generated documentation website.
// jsdoc.js
/** @module firstDoc */
//The rest of the code goes here
Converting JSDoc Files
One of the biggest plusses of using JSDoc is the ability to convert JSDoc files to either produce a documentation website or even to Typescript so they can reap the benefits of using Typescript like catching errors at compile time, integration with Typescript projects, etc.
Generating Documentation Website From JSDoc Files
As stated above, you could produce a more readable GUI by following the steps below:
- Install jsdoc
npm install -g jsdoc
- Run
jsdoc
for the target file
jsdoc path/to/file.js
- Open the generated website
The
jsdoc
CLI automatically creates anout
folder where the files reside. Go toout/index.html
and open it in the browser.
This is what the default jsdoc
generated template looks like, but you could configure the template to look differently.
Generating .d.ts files from JSDoc
.d.ts
files in TypeScript represent declaration files that contain types accessible by all .ts
files in a project. You can generate these files from the JSDoc code using the following steps:
- Install
tsd-jsdoc
in the project folder
npm install tsd-jsdoc
- Generate .d.ts files
For A Single File
jsdoc -t node_modules/tsd-jsdoc/dist -r our/jsdoc/file/path.js
For Multiple Files
jsdoc -t node_modules/tsd-jsdoc/dist -r file1.js file2.js file3.js ...
For Entire Folders
jsdoc -t node_modules/tsd-jsdoc/dist -r src
It combines all types from file(s) into a single file in out/types.d.ts
.
Note: This assumes you have installed jsdoc
from the
previous section. If not, go ahead and install it first before running this step.
Conclusion
At this point, we have learned the basics of using JSDoc as well as generating types and documentation websites from the JSDoc code. JSDoc is specifically useful in cases where your Typescript compile time/build step causes an inverse effect on productivity. It is also useful when working with legacy codebase.
Rich Harris (Creator of Svelte and SvelteKit) moved the entire Svelte and SvelteKit repository from TypeScript to JSDoc instead. TypeScript has also added support for many of the JSDoc declarations (source)
Resources
- Full Source Code Of This Article - Github
- JSDoc official documentation
- Moving Svelte 4 from TS to JSDoc - Rich Harris
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.