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 = {};
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.

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

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.

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

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
  • 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

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.

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 either need to switch, or create an additional special {name}.tsconfig.json file just for emitting (such as emit.tsconfig.json)

      • If you create a *.tsconfig.json file, add "$schema": "https://json.schemastore.org/tsconfig" as a top level property so you get intellisense 🤓
    • 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 emit.tsconfig.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.

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

Markdown Source Last Updated:
Sat Jul 25 2020 18:14:09 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