Joshua's Docs - JSDoc Cheatsheet and Type Safety Tricks
Light
help

Warning: A lot of this page assumes you are using VSCode as your IDE, which is important to note because not all IDE's handle JSDoc the same way. I also focus on how JSDoc can provide type-safe JS through VSCode's TypeScript engine.

Table of Contents

Resources

What & Link Type
Official JSDoc Docs Official Docs
devdocs.io/jsdoc Searchable Docs
Devhints Cheatsheet Cheatsheet
theyosh.nl Cheatsheet Printable Cheatsheet

Advanced Usage

Type Guards

This is pretty crazy, but you can actually implement a type-guard completely in JSDoc! After a bunch of digging, I found the right syntax thanks to this Github issue comment.

Syntax:

// Where YOUR_TYPE is understood type (either naturally inferred, or through JSDoc)

/**
 * @param {any} value
 * @return {value is YOUR_TYPE}
 */
function isYourType(value) {
	let isType;
	/**
	 * Do some kind of logical testing here
	 * - Always return a boolean
	 */
	return isType;
}
Full Example:
// @ts-check
/**
 * @typedef {{model: string}} Robot
 */

/**
 * @typedef {{name: string}} Person
 */

/**
 * @param {object} obj
 * @return {obj is Person}
 */
function isPerson(obj) {
	return 'name' in obj
}

/**
 * Say hello!
 * @param {Robot | Person} personOrRobot
 */
function greet(personOrRobot) {
	if (isPerson(personOrRobot)) {
		// Intellisense suggests `.name`, but not `.model`
		console.log(`Hello ${personOrRobot.name}!`);
		// Below will throw type error
		console.log(personOrRobot.model);
	} else {
		// Intellisense suggests `.model`, but not `.name`
		console.log(`GREETINGS ${personOrRobot.model}.`);
	}
}

Annotating destructured parameters

If you want to annotate the type of variables that are assigned via destructuring, it can be a little complicated with JSDoc.

For destructuring directly, it looks like this:

/**
 * @typedef {object} DestructuredUser
 * @property {string} userName
 * @property {number} age
 */
/** @type {DestructuredUser} */
const {userName, age} = getUser();

For annotating on a function argument:

/**
 * @param {object} obj
 * @param {string} obj.userName
 * @param {number} obj.age 
 */
function logUser({userName, age}){
	console.log(`User ${userName} is ${age} years old.`);
}

... Or with a separate typedef

/**
 * @typedef {object} DestructuredUser
 * @property {string} userName
 * @property {number} age
 */
/** @param {DestructuredUser} param */
function logUser({userName, age}){
	console.log(`User ${userName} is ${age} years old.`);
}

You can even annotate on arrow functions:

/**
 * @param {object} obj
 * @param {number} obj.total
 * @param {string} obj.vendorName
 */
const printReceipt = ({total, vendorName}) => {
	// total and vendorName will have correct types! :)
}

Annotating function signatures / function as a type

This looks pretty similar to TS - return type should come after arguments with colon:

/**
 * @typedef {function(string): string} StringProcessor
 */

// It is far easier to write as a multi-line typedef with arg names
/**
 * @typedef {function} StringProcessor
 * @param {string} inputStr - String to transform
 * @returns {string} Transformed string

Here are some practical examples:

/**
 * @typedef {function(string): string} StringProcessor
 */

// Annotating single variable
/**
 * @type {StringProcessor}
 */
const upperCaser = (input) => {
	return input.toUpperCase();
}

// Within another typedef
/**
 * @typedef {Object<string, StringProcessor>} StringProcessorsCollection
 */
/**
 * @type {StringProcessorsCollection}
 */
const myStringMethods = {
	lowerCase: function(input) {
		return input.toLowerCase();
	}
}

Classes and Constructors

In general, documenting ES2015 (ES6) classes and their associated parts (constructor, methods, etc.) is well-supported in JSDoc. You can find some great examples of how to do this in:

However, there are some additional notes I would like to add:

  • You can document a class outside of its implementation, however, this is incredibly complex with pure JSDoc, and I would strongly recommend just using a .ts file for this type of use-case.

    • This is hard to do in general, and there are not many good reasons to be doing it anyways
    • This is an example of defining a constructor on an interface
  • Trying to document a class that is structured as an IIFE with JSDoc is... very difficult. In fact, I would recommend never even attempting to do so; I'm fairly certain there might be some open bugs that make this almost impossible

    • This is especially true if you are also trying to generate declaration files files from your JSDoc; I kept running into a "Declaration emit for this file requires using private name" error
    • I think most of the ways to get this to work require a lot of explicitly creating types outside of the IIFE, then importing those types in and merging them; lots of extra work

Casting and Type Coercion in JSDoc

At least in @ts-check land, you probably won't have to deal with casting in JSDoc much. For example, usually annotating the type during assignment is enough to cast an any to a specific type.

However, for casting unknown or situations with inferred "no overlap", there are some other escape hatches.

Simple Casting

One way is to wrap with parenthesis during assignment:

/** @type {unknown} */
let huh;

/**
 * Works!
 */
/** @type {number} */
const success = (huh);

/**
 * FAILS
 */
/** @type {number} */
const failed = huh;

TIP: Many formatters remove "unnecessary" parenthesis wrapping (such as Prettier), but they will preserve it if you inline the JSDoc annotation with the code. Like so:

const success = /** @type {number} */ (huh);

Advanced Casting

If the above didn't work, you are probably in a "neither type sufficiently overlaps" situation. I actually couldn't find this documented anywhere, but this worked for me, although it is not pleasant to look at!:

/**
 * @typedef {object} Person
 * @property {string} name
 * @property {number} age
 */

/** @type {number} */
let something;

/**
 * Works!
 */
const success = /** @type {Person} */ (/** @type {unknown} */ (something))

/**
 * FAILS
 */
/** @type {Person} */
const failed = (something);

WARNING: Linters might automatically remove the parenthesis around the inner-most cast, which breaks this. You should be able to disable the linter on that specific line by following your specific linter instructions.

For prettier: // prettier-ignore disables for the next line.

WARNING: With the above solution, VSCode does not like trying to cast to the same variable (reassignment). Just create a new variable and copy into it while casting if you are trying to change the type of an existing one.

Importing external / exported types directly in JSDoc

This is kind of a crazy feature but, at least in VSCode, you can actually import types from specific modules by using import directly within a JSDoc block! For example, if we wanted to explicitly type a variable as NodeJS's "fs" -> "Stats" type, we could use:

/**
 * @type {import('fs').Stats}
 */
let fsStats;

// Or, via typedef
/** @typedef {import('unist').Node} AstNode */

You can read a bit more about this feature (implemented in 2018) in various spots:

Importing Non-TS Code and/or CommonJS Module.Exports

If you try to use the above trick to import something that is not a type, and/or specifically part of a CommonJS module.export = {} pattern, you might get a "namespace has no exported member" error. Example:

/**
 * @typedef {typeof import('../my-js-file').myExportedFunc} MyFunc
 */

// ERROR: Namespace '"___/my-js-file".export=' has no exported member 'myExportedFunc'.ts(2694)

This appears to be a bug, and there is an open issue to track it (#29485), however, luckily in the meantime there is a workaround! - using bracket notation instead of dot notation.

Thanks to this comment, I have tried and verified that the following works:

// Use `import(FILE_PATH)['EXPORTED_THING']` instead of `import(FILE_PATH).EXPORTED_THING
/**
 * @typedef {typeof import('../my-js-file')['myExportedFunc']} MyFunc
 */

VSCode - JavaScript Type Safety with JSDoc

Intro to Type Safety with JSDoc

VSCode has an awesome baked-in feature, which is that it can provide JS type-safety tooling, powered by JSDoc comments. This is built-in to VSCode and requires no extra tooling, transpiling, or even config files.

How to Trigger the JS Type Checker

Triggering the built in JS Type Checker options:

  • Add //@ts-check to top of JS file
  • Add setting to preferences:

    • "javascript.implicitProjectConfig.checkJs": true

      • Can be added to either global or workspace settings.json
  • Add a tsconfig.json or jsconfig.json file in the project to control the settings (and add checkJs: true)

    • If using tsconfig.json, also add allowJs: true

Although these built in options internally ("salsa" engine?) actually use TypeScript features to do the checking/linting (you can read more about that here), they use JSDoc to augment the interpretation/inferred types of JS.

Programmatically Scanning JS for Type Issues

You might notice that VSCode tends to only check and report type problems on JS files that are currently open, instead of every JS file in the workspace. To get around this, you have a few options:

  • Check files with the TypeScript compiler, directly

    • You can actually just run your JS through TSC, even without any TS or a tsconfig. Use noEmit to make sure no files are output.

      • Example: tsc --noEmit --project jsconfig.json
    • This is also a way to use JSDoc for type-safety, even without VSCode! However, it requires that you install the TypeScript compiler globally.
    • NOTE: Even if you run this command from within VSCode, it won't make the problems caught automatically show up in the VSCode problems panel.
  • Use VSCode tasks and problemMatcher option

    • If you want all problems to be reported to and displayed in VSCode, you are (currently) forced to create a custom task. This will let VSCode run the TSC command directly and use the problemMatcher option to help it understand how to interpret the CLI response.
    • See this S/O and this open issue thread for VSCode
  • Try out the experimental enableProjectDiagnostics setting (see this S/O)

Simple Example:

WARNING: JSDoc TS annotations MUST be in a multi-line comment block (/** ___ */) comment to be be parsed. They are ignored in single line (//) comments!!!

/**
 * @type {google.maps.StreetViewPanorama}
 */
let pano = this.mapObjs.svPano;

/**
 * @type Array<{localPath:string, fullPath: string}>
 */
let filePaths = [];

/**
 * @typedef {"Hello" | "World"} MyStringOptions
 */

// You can even re-use types!
/**
 * @typedef {object} Person
 * @property {string} name - Person's preferred name
 * @property {number} age - Age in years
 * @property {boolean} likesCilantro - Whether they like cilantro
 * @property {string} [nickname] - Optional: Nickname to use
 */


/**
 * @type Person
 */
let phil = {
	name: 'Phil Mill',
	age: 25,
	likesCilantro: true
}

/**
 * @type Person
 */
let bob = {
	name: 'Robert Smith',
	age: 30,
	likesCilantro: true,
	nickname: 'Bob'
}

/**
 * 
 * @param {Person} personObj 
 */
function analyzePerson(personObj){
	// Intellisense will fully know the type of personObj!
	console.log(personObj.name);
}

VSCode - Advanced JS Type-Safety with JSDoc

Package Types from node_modules

And you can even install module type defs, like those from @types. This works the same as in TypeScript, so see my notes about "including external type definition files" for details. For most JS projects, since you are likely missing a tsconfig.json file, the easiest option might be a one-off triple-slash directive usage.

For pulling in types from a module, one at a time, you can use the import syntax within JSDoc. I already have a section on this elsewhere on this page.

This is an extremely helpful section in the VSCode docs: "Typings and Automatic Type Acquisition"

JSDoc Globals in VSCode

Unfortunately, for globals, it looks like JSDoc or inline comments are not supported (issue #15626, doc change) the only way to tell VSCode about those is through a TypeScript "ambient declaration file" (e.g. *.d.ts file). For example, you can create a globals.d.ts file, declare your globals / interfaces / etc., and VSCode's type checker will pick up on it, despite your project being JS code.

You can also change how you declare them; explicitly creating via global.{yourVarName} (or globalThis) will suppress cannot find name {yourVarName} errors, and the IDE should even preserve the type if the variable is read in a different file.

Scripts vs Modules, Block-Scoped Variables

The default behavior of TS is that a file is a module if it imports or exports anything, otherwise it is a script. And, if it is a script, due to the lack of closure, top level variables become global. Sometimes this is accurate, for example in this setup:

<!-- Files:
 - index.html
 - file_A.js
 - file_B.js
 -->
<!-- index.html -->
<script src="./file_A.js"></script>
<script src="./file_B.js"></script>

If both files A and B are scripts, not modules, and both have a top level variable declared as const myStr = 'hello';, you will get an error:

Cannot redeclare block-scoped variable 'myStr'.ts(2451)

Again, in the above scenario, this makes sense; VSCode and TS are correct. If we wanted to force these files to be isolated from each other, while still running in index.html, we could:

  • Change the script tags to use <script type="module"> for ES6 module encapsulation (caniuse)

However, this actually don't tell VSCode to interpret the files any differently, and the error wont go away. Furthermore, VSCode will continue to throw the error even if the files are scripts, but won't conflict due to directory separation. For example:

 - jsconfig.json
 - dir_A/
	 - index.html
	 - file_A.js
 - dir_B/
	 - index.html
	 - file_B.js

Even if file_A is only used in dir_A/index.html, and file_B is only used in dir_B/index.html, TSC will still complain about any top-level variables that exist in both files and it thinks will conflict (they won't). How do we explicitly tell TSC about this?

  • If we want to force these files to act like modules, we can add an empty export declaration:

    • Syntax

      • ESM: export {}
      • CJS: module.exports = {}
    • In HTML, you also need to update script tags to use <script type="module"> when importing
    • This also prevents variables from becoming globals, which might be something that you actually don't want to lose.
  • For our exact scenario, with pure JS in a web environment, our options are:

    • Move config files down

      • Add: /dir_A/jsconfig.json
      • Add: /dir_B/jsconfig.json
      • Delete: /jsconfig.json
      • Reload your workspace and/or TS engine
    • Give top-level variables closure (e.g. by wrapping in an IIFE)

      // Change this...
      const myStr = 'hello'
      console.log(myStr);
      
      // ...to this:
      (() => {
      	const myStr = 'hello';
      	console.log(myStr);
      })();
    • Change variable type to something that is not block-scoped

      • Change const or let to var

Also, although you might think it would work, turning off / deleting all config files and then individually adding the // @ts-check directive to each file does not fix it

I've asked for a better solution to this problem on StackOverflow. If you know of a better approach, you should respond there, so you can claim some credit!

How to ignore errors in multi-line blocks (e.g JSX)

You basically can't, at least not at the moment - see:

VSCode Type-Safety JSDoc - Issues

If you run into issues with this, here are some things to check or try:

  • "cannot find name ___ ts(2304)"

    • VSCode seems to have an issue with ambient declaration files (*.d.ts) being picked up without a config file. Any of the following should work:

      • Add a tsconfig.json file (or jsconfig.json, as a fast alternative)

        • Bare-bones fix: add jsconfig.json with contents of {} or something like {"exclude": ["node_modules"]}
      • Use a triple slash directive at the top of the file, for example: /// <reference path="../types.d.ts"/> or /// <reference types="googlemaps"/>
      • Keep the ambient type file open while editing (right click tab, "Keep Open") - quick hack
    • Can you add an import that brings in the namespace'd type?
    • Is the type declared under a namespace or module that doesn't match where you are trying to use the type?
    • If you have used import or export in a TS file that you are trying to use as an ambient type declaration file, make sure types are wrapped with declare global {}.
    • If using a tsconfig.json file, use tsc --listFiles --p {pathToTsConfigJson} to help see what files are getting included (kudos)
  • Type assigned to CommonJS (require()) variable is typeof import(), and you aren't getting the correct type that corresponds with the exported variable

    • Instead of const myImport = require('my-lib'); try const myImport = require('my-lib').default;
    • The above is actually a TS / CommonJS interop problem, not a JSDoc problem
  • Using a tsconfig.json for checking JS doesn't seem to be working / rules are not enforced

    • Do you have both allowJs and checkJs on?
    • Check for outdated / mismatched TypeScript versions. You can force VSCode to use a newer, or specific version of TS, by following these steps

      • You can always install typescript as a devDependency, and then use settings.json to tell VSCode to use it as the active version
    • As a last resort, if you really don't need full TS support, try just using a jsconfig.json config instead (which supports most of the same options)

Important reference material

Further reading

Here are some more guides on trying to get the benefits of TypeScript in JS (type-safety):


Generating Files From JSDoc

Some important links:

Emitting Type Definitions / Declarations from JSDoc Types

The main benefit of using JSDoc for type checking is really just the improvement of the IDE experience; unlike traditional TypeScript, you are not actually compiling your code.

However, if you are building a library, such as an NPM package, you might actually want to generate some output files; mainly a type declaration file.

This page from the TS Handbook covers how to setup a JSDoc project to output declaration files, but I'll also summarize below:

If you want to emit a types file based on JSDoc, the main thing you need to do is:

  • Make sure you are using a tsconfig.json file

    • If you are using a jsconfig.json file, you will probably want to switch, or create an additional special tsconfig.{name}.json file just for emitting (such as tsconfig.emit.json)

      • You can use jsconfig.json files with TSC, but there are some related issues you might run into
    • At a bare minimum the config needs to have:

      {
      	"compilerOptions": {
      		"allowJs": true,
      		"declaration": true,
      		"noEmit": false
      	}
      }
    • You probably also want:

      • "module": "commonjs"
      • "moduleResolution": "node"
      • "esModuleInterop": true
    • You can use "emitDeclarationOnly": true if you don't want to copy the JS files and/or modify them in any way for distribution

      • If you do want TSC to copy the JS files over, it can even handle some basic transpiling for you! Use the compilerOptions.target to specify the desired output ES version.
    • Additionally, use compilerOptions.outDir or compilerOptions.outFile to control the location and/or name of the declaration file(s)

      • If you want a single emitted file, use outFile (but this be warned this is incompatible with commonjs)
  • Add typescript as a dev dependency, so everyone can use tsc
  • Call tsc with the config, which will create the file

    • If you have a special named config, pass via the --project flag.
    • tsc --project tsconfig.emit.json
  • Add the (final) generated *.d.ts file (or directory that contains the generated files) to your package.json, under the types field

Be very careful using *.d.ts files in your source code, especially if you are relying on generated declaration files. Ambient declaration files (*.d.ts) are essentially used as "guides" to the compiler; they are not considered for output file generation and do not emit any JS.

You also need version >= 3.7 of TypeScript in order for this to work (Ref A, Ref B), Ref C

There are alternative generators other than the TypeScript compiler, such as tsd-jsdoc, but TSC is recommended if it can work for your situation.

Where to Output JSDoc Generated TS Declaration Files

When using TSC to produce *.d.ts files from JS source code, you have a few options on where to place those files. The common options seem to be:

  • Don't transpile JS (emitDeclarationOnly: true), and emit to /types(via outDir)

    • You can optionally use declarationDir instead of outDir to control where types are emitted
  • Transpile JS, and emit JS + TSD to /dist (via outDir)
  • Don't transpile JS, and don't emit to a separate folder; emit alongside JS files: this is basically the default config

    • Negative: this complicates organizing src code vs published code

      • Many people argue that generated files should not be in version control; if you follow this advice, you would need to carefully gitignore the specific generated *.d.ts files that are among your source files, but also make sure they are published to NPM

Emitting a Single Declaration File

In general, bundling all the emitted types into a single declaration file is not really necessary or possible with TS repos (see my disclaimer in my package authoring notes), but for generating from JSDoc, you can easily accomplish this with emitDeclarationOnly: true, and the outFile option.

Another approach - the tsd-jsdoc library is oriented towards this goal. Here is a guide on using it.

Caveats for Using JSDoc to Generate Declaration Files

The biggest caveat with using JSDoc to generate declaration files (especially for a NPM package entry point) is that (currently) it feels like an "all-or-nothing" approach. There is a limited set of controls to alter the behavior of the generated files (to be fair, JSDoc is not really the main purpose of TSC 😂), and once the files are generated, they aren't really meant to be hand-edited.

So, you usually see one of a couple options:

  • Authors hand-edit the TS declaration files that are included in distribution, and also check them into VC (example). TSC is not used for generation.
  • Authors use TSC for declaration generation, omit the files from VC (e.g. in /dist), and don't modify the generated output
  • Authors use TSC for declaration generation, but include the generated (vendored) declaration files in VC

    • This is usually combined with outFile to force a single declaration file, and having it created alongside the rest of the source files
    • Although this is checked into VC, not wise to hand-edit after generation, since changes will be wiped out by future generations
  • Authors only use TSC to generate the initial set of types, and then hand-edit them afterwards

    • I would not recommend this approach. I don't consider it a good idea to maintain duplicate type definitions (inline via JSDoc, and the declaration files). Plus, running TSC will overwrite your changes. This speaks to the "all or nothing" problem...
  • Authors merge generated declaration files with hand-coded types

    • As a build step, you can always copy over hand-coded declaration files that can supplement the generated files from TSC
    • This is somewhat common, especially for *.d.ts files, since those are not bundled with TSC's generated output

How To Export Types from Index Entry Point

If you are using TSC to generate declaration files from pure JavaScript with JSDoc, you might run into the funny question of how to export types (not implementations) from the entry-point (e.g. index.js) of your library / app.

Normally, in a TypeScript project, you can easily export types just the same as you can export functions, classes, etc. You might even do something like export * from './my-types', where you import and then re-export types that your consumers will need, from within the entry-point.

But, in JS files, types only exist in JSDoc comments; how do you export from JS-land?

Options:

=== Export types one-by-one, with @typedef blocks ===

/**
 * @typedef {import('./my-types').Dog} Dog
 */
/**
 * @typedef {import('./my-types').Cat} Cat
 */

Even if you never use the imported type within the JS file you declare it in, TSC will export them in the generated *.d.ts file!

=== Export types in bulk ===

Currently not supported (AFAIK, and backed up by this comment by TruckJS) 😢

Ideally, this would work the same way it does in TypeScript, where you could write something like:

// These are NOT supported, and are merely for illustrative purposes
/**
 * @typedef {import * from 'my-types'} MyTypes
 *  - Or maybe,
 * @typedef {import('my-types') as MyTypes}
 *  - Or,
 * @export {import('my-types')}
 */

None of these are supported, but some of the syntax has been discussed / proposed on this thread, and here. Also related is #14377.

=== Manual Declaration Editing ===

Of course, if you are hand-editing the declaration files that ship with your JS, you can add whatever you want.

You could also automate declaration editing; you could theoretically generate the bulk of the files with TSC, and then append the types you want exported (in bulk) with a simple build script.


Issues

Issue with Number Properties

Using a number as an object key is completely permissable in JavaScript, so long as you use bracket notation for accessing. For example:

const levels = {};
levels[0] = 'low';
levels[1] = 'medium';
levels[2] = 'high';
console.log(levels[0]);
// > 'low'
// You can also access by string
console.log(levels['0']);
// > 'low'

However, if you try to document this with JSDoc, you might run into an issue...

/**
 * @typedef {object} Levels
 * @property {string} 0
 * @property {string} 1
 * @property {string} 2
 */

// ERROR: TS: Identifier expected.

It looks like VSCode's integration with JSDoc for type-checking does not like anything other than a standard a-Z character as the leading property key.

Number Property Key Workaround

  • One workaround is to simply define the entire object within the first typedef line:
/**
 * @typedef {{0: string, 1: string, 2: string}} Levels
 */

// No errors! :)
  • Another workaround would be to use an actual TS file, e.g. types.d.ts, and follow my directions on having VSCode pick up on it.
Markdown Source Last Updated:
Sun Nov 15 2020 04:14:30 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Tue Mar 10 2020 23:28:08 GMT+0000 (Coordinated Universal Time)
© 2020 Joshua Tzucker, Built with Gatsby
Feedback