JSDoc Evangelism
Peter Vivo
Posted on October 27, 2024
TL;DR
Working with a legacy codebase—something many of us can't avoid from time to time—led me to try JSDoc instead of TypeScript. I have to reveal the surprising truth!
First of all, let's clarify: JSDoc or TS simply means enhancing the developer experience (including later review, reuse, or understanding in any environment: git web page, random editors, Chrome, Firefox DevTools, Vim, cat, etc.).
The First Step
Just open your editor, write a proper comment, and test if JSDoc is working.
/** @typedef { 'JS' | 'JQuery' | 'TS' | 'JSDoc' } Phase; */
/**
* On typing ' editor will popup string list.
*
* @type {Phase}
*/
const badCase = 'React'; // !!!! lint alert !!!!
/** @type {Phase} */
const goodCase = 'JQuery';
JSDoc vs. TypeScript
In my experience, JSDoc can replace TypeScript in many scenarios, with the added bonus of interoperability between the two.
The biggest advantage of JSDoc, in my opinion, is that it uses a standard JS comment system, which means it won't break any JavaScript code and will run everywhere JS can run.
Feature | JSDoc | TypeScript |
---|---|---|
Compilation | No need to compile the code | Mandatory to compile |
Dependency | Works without any dependencies | Requires TypeScript as a dependency |
Namespace | Doesn't interfere with types and other imports—you can use the same name for components and types in React, for example | TypeScript names can conflict with other imports |
Rework | No need to change existing code, just add a comment | Some parts of your code need to be rewritten in TS |
Legacy Code | Can be used when transforming to TypeScript isn't an option—many legacy projects fall in this category | Requires management approval to modify the project |
Incremental Adoption | You can use JSDoc where you need it—even in new commits only, easily searchable for future changes | Can be introduced gradually but still requires modifying build systems |
Future Flexibility | Can easily be translated to TypeScript later since they use similar under-the-hood mechanics | Requires work to revert back to JS if needed |
Runtime Expectations | Since it's just a comment, it's clear that it doesn't perform runtime type checks, just like TypeScript | TypeScript may give the impression of runtime type safety |
JSDoc Editor Experience
I can write JSDoc in any editor, but it's rare for editors to fully understand it.
Node Module Experience
I also created an npm module, jsdoc-duck, as a JSDoc-coded module. This highlighted that without TypeScript, creating a JSDoc npm module isn't straightforward. Maybe if I spent more time figuring out Vite build parameters, a solution could be found. But the good news is that you don't need to use that module via npm—you can simply copy index.js to a local place, which avoids adding a new dependency and mitigates any risks if the module owner makes breaking changes.
Wormhole Between JSDoc and TypeScript
The good news is that TypeScript and JSDoc are compatible. JSDoc uses slightly different syntax, but you can use JSDoc module types in TypeScript projects and vice versa. Ultimately, the choice is yours.
Follow the Yellow Brick Road
VS Code is a great example of how well it can show your types in JSDoc code—in my opinion, it's surprisingly low-noise compared to TypeScript.
Bonus
VS-Code Snippets
"@type": {
"prefix": ["ty"],
"body": ["/** @type {$0} */"],
"description": "jsdoc type"
},
"@typedef": {
"prefix": ["td"],
"body": ["/** @typedef {$1} Foo */"],
"description": "jsdoc typedef"
},
JSDoc-Duck Code Example
In this view, syntax highlighting doesn't help to understand the types. However, this short program is a good example of how JSDoc can use TypeScript's advanced features too.
import { useMemo, useReducer } from "react";
/**
* @template T - Payload Type
* @typedef {T extends { type: infer U, payload?: infer P } ? { type: U, payload?: P } : never} ActionType
*/
/** @template AM - Actions Map @typedef {{ [K in AM['type']]: K }} Labels */
/** @template AM - Actions Map @typedef {{ [T in AM["type"]]: Extract<AM, { type: T }> extends { payload: infer P } ? (payload: P) => void : () => void }} Quack */
/**
* @template ST - State
* @template AM - Actions Map
* @typedef {(state: ST, action: AM) => ST} Reducer
*/
/**
* Factory function to create a typed action map.
* @template AM - Actions Map
* @param {Labels<AM>} labelsObject - The keys representing action labels.
* @param {function} dispatch - The dispatch function for actions.
* @return {Quack<AM>} The resulting typed action map.
*/
export const quackFactory = (labelsObject, dispatch) => Object
.keys(labelsObject)
.reduce(
/**
* @arg {Quack<AM>} acc
* @arg {keyof Labels<AM>} type
* @return {Quack<AM>}
*/
(acc, type) => ({
...acc,
[type]: (payload) => {dispatch({ type, payload });}
}), {});
/**
* A factory hook to create a state and a typed dispatch functions\
* @exports useDuck
* @template AM - Actions Map
* @template ST - State Typer
* @param {(st: ST, action: AM) => ST} reducer - The reducer function to manage the state.
* @param {ST} initialState - The initial state value.
* @return {[ST, Quack<AM>]} The current state and a map of action dispatch functions.
*/
export const useDuck = (reducer, initialState, labels) => {
const [state, dispatch] = useReducer(reducer, initialState);
const quack = useMemo(
() => quackFactory(labels, dispatch),
[dispatch, labels]
);
return ([state, quack]);
};
Happy coding! Borrowing, instead of adding dependencies, can simplify things.
Posted on October 27, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.