Joshua's Cheatsheets - 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

Documenting Objects

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 = {};
myObj[0] = 'low';
myObj[1] = 'medium';
myObj[2] = 'high';
console.log(myObj[0]);
// > 'low'
// You can also access by string
console.log(myObj['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.

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 thow 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();
	}
}

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:

VSCode - JavaScript Type Safety with JSDoc

Intro

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

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.

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.

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.

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

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

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

Important reference material

Further reading

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

Markdown Source Last Updated:
Sun May 24 2020 19:55:59 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