JSDoc Evangelism

pengeszikra

Peter Vivo

Posted on October 27, 2024

JSDoc Evangelism

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';
Enter fullscreen mode Exit fullscreen mode

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.

syntax highlight of JSDoc by VSCode

Bonus

VS-Code Snippets

  "@type": {
    "prefix": ["ty"],
    "body": ["/** @type {$0} */"],
    "description": "jsdoc type"
  },

  "@typedef": {
    "prefix": ["td"],
    "body": ["/** @typedef {$1} Foo */"],
    "description": "jsdoc typedef"
  },
Enter fullscreen mode Exit fullscreen mode

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]);
};
Enter fullscreen mode Exit fullscreen mode

Happy coding! Borrowing, instead of adding dependencies, can simplify things.

💖 💪 🙅 🚩
pengeszikra
Peter Vivo

Posted on October 27, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

JSDoc Evangelism
javascript JSDoc Evangelism

October 27, 2024