More on How JSDoc Saved the Day: ESLint Enters the Chat

Previously, I showed how you can use JSDoc and jsconfig.json as a replacement for TypeScript style type checking — without a build step, no less. At the end of that post I teased an ESLint follow-up. And here we are, I’m happy you’re here.

First Things First

Before jumping in, I wanted to address a great comment on my previous post: why do you even need ESLint if jsconfig.json already gives you type checking?

The answer: jsconfig only catches type errors. VSCode will tell you when you pass a number where a string should go. But it won’t tell you that you forgot to document a function completely. It won’t catch that you added a parameter but forgot to add the corresponding @param for it. It also won’t notice that your JSDoc says one thing and your code says another.

That’s the gap ESLint fills. I hope that explains the why. This post assumes you’ve already set up JSDoc and are looking to add some quality-of-life configuration. If you wish to see how I set up JSDoc within VSCode, go here. Okay!

Let’s get started. 🤘

First, we will need to install a handful of dependencies. I’m using npm, but feel free to use your favorite package manager. We’ll be installing:

  • eslint — the linter itself
  • eslint-plugin-jsdoc — the rules that actually understand JSDoc comments
  • @eslint/js — ESLint’s own recommended rule set (catches stuff like unused variables)
  • globals — predefined global variables for Node, browser, etc.

Here’s the script

  npm install --save-dev eslint eslint-plugin-jsdoc @eslint/js globals

ESLint uses a “flat config” format. Create an eslint.config.js in your project root:


  const globals = require("globals");
  const js = require("@eslint/js");
  const jsdoc = require("eslint-plugin-jsdoc");

  module.exports = [
    {
      files: ["**/*.js"],
      languageOptions: {
        globals: {
          ...globals.node,
        },
        parserOptions: {
          ecmaVersion: "latest",
          sourceType: "commonjs",
        },
      },
      plugins: {
        jsdoc,
      },
      rules: {
        ...js.configs.recommended.rules,
        // JSDoc rules
        "jsdoc/require-jsdoc": [
          "warn",
          {
            publicOnly: true,
            require: {
              FunctionDeclaration: true,
              MethodDefinition: true,
              ClassDeclaration: true,
            },
          },
        ],
        "jsdoc/require-description": "warn",
        "jsdoc/require-param": "warn",
        "jsdoc/require-returns": "warn",
        "jsdoc/check-param-names": "warn",
        "jsdoc/check-tag-names": "warn",
        "jsdoc/check-types": "warn",
      },
      ignores: ["node_modules/**"],
    },
  ];

Yeah, it’s a lot of boilerplate but let’s quickly go over what matters.

Every rule is set to “warn”. ESLint has three levels: “off” disables a rule entirely, “warn” shows a yellow squiggly but won’t fail your build, and “error” shows red and will cause eslint to exit with a failure code. I like “warn” as a starting point — it tells you something’s wrong without blocking you. You can always promote rules to “error” if you’re that type of person.

It’s the rules

The JSDoc rules themselves:

  • require-jsdoc — flags exported functions with no JSDoc at all
  • require-description — catches empty JSDoc blocks (just tags, no explanation)
  • require-param — flags function params missing an @param
  • require-returns — flags functions that return something but don’t declare @returns
  • check-param-names — catches @param names that don’t match actual parameter names
  • check-tag-names — catches misspelled or invalid JSDoc tags
  • check-types — catches invalid type expressions

One thing worth calling out: that publicOnly: true on require-jsdoc. This means it only enforces JSDoc on exported functions. Non-exported helper functions won’t trigger a warning. So remove the publicOnly flag if you want everything documented.

Here’s how I’d roll it out gradually:

  • Start with require-jsdoc and require-param. These catch the biggest gaps — undocumented functions and missing parameters. High value, low noise.
  • Add check-param-names and check-types next. These catch stale or wrong docs, which are arguably worse than missing docs. At least missing docs are honest.
  • Save require-description for last. It can get noisy. Leave it as a warning permanently if you want — no shame in that.

It’s 2026, so go ahead and throw a lint script in your package.json for your CI pipelines while you’re at it:


{
"scripts": {
"lint": "eslint ."
}
}

So now what you’ve got is:

  1. JSDoc annotations — you document your types and intent right in the code
  2. jsconfig.json + // @ts-check — VS Code validates those types as you type
  3. ESLint + eslint-plugin-jsdoc — enforces that JSDoc actually exists, is complete, and is correct

I think this is pretty slick. JSDoc is the source of truth. jsconfig makes sure the types are right. And ESLint makes sure the documentation is actually there.

The perfect squiggle setup.

Carl Carlson from the Simpsons doing

But wait, there’s more: adding prettier.

ESLint tells you what’s wrong with your code. Prettier tells you what’s wrong with how it looks — inconsistent indentation, missing semicolons, trailing commas, all of that. It auto-formats on save too, which I’d be lost without. The catch is that ESLint also has some formatting opinions, and they don’t always agree with Prettier’s. eslint-config-prettier fixes that by turning off any ESLint rules that would conflict.

  npm install --save-dev prettier eslint-config-prettier

Then update your config:

  const eslintConfigPrettier = require("eslint-config-prettier");

  module.exports = [
    {
      // ...your existing config
      rules: {
        ...js.configs.recommended.rules,
        ...eslintConfigPrettier.rules,
        // ...your JSDoc rules
      },
    },
  ];

Then you’re set! ESLint handles the logic, Prettier handles the formatting, and they stay out of each other’s way.

Until next time 👋🫡

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *