Type-Checking JavaScript with JSDoc

You don't have to rename files to .ts to benefit from TypeScript. The same checker can analyse plain JavaScript, reading types from JSDoc comments. It's a gentle way to add safety to an existing project, and some libraries use it permanently to avoid a build step.

Turning on checking

Add // @ts-check as the first line of any .js file, and editors that use TypeScript (like VS Code) will start reporting errors in it:

JS cart.js
// @ts-check
const cart = { items: [], total: 0 };

cart.totl = 10; // error: Property 'totl' does not exist. Did you mean 'total'?

Even without any annotations, TypeScript infers a lot from plain JavaScript. To check every JavaScript file in a project instead, use tsconfig.json (or jsconfig.json):

JSON tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "noEmit": true,
    "strict": true
  },
  "include": ["src"]
}

Then npx tsc type-checks the JavaScript without producing output. To exempt a single file, add // @ts-nocheck at its top.

Annotating with JSDoc

Types go in specially formatted comments that start with /**. The type itself uses normal TypeScript syntax inside braces:

JS price.js
// @ts-check

/**
 * Applies a percentage discount.
 * @param {number} price - original price
 * @param {number} [percent=10] - optional, defaults to 10
 * @returns {number}
 */
export function discount(price, percent = 10) {
  return price * (1 - percent / 100);
}

/** @type {string[]} */
const tags = [];

discount("20"); // error: Argument of type 'string' is not assignable to parameter of type 'number'.

Square brackets around a parameter name mark it optional. The descriptions also show up in editor tooltips, so JSDoc doubles as documentation.

Defining object types

@typedef creates a named type, like a type alias:

JS user.js
// @ts-check

/**
 * @typedef {object} User
 * @property {number} id
 * @property {string} name
 * @property {"admin" | "member"} role
 * @property {string} [email]
 */

/** @param {User} user */
export function greet(user) {
  return `Hello ${user.name}${user.role === "admin" ? " (admin)" : ""}`;
}

/** @typedef {{ x: number, y: number }} Point */

Both the long @property form and the inline object form work. You can also import types from other files — including .ts files — with @import (TypeScript 5.5+):

JS orders.js
// @ts-check
/** @import { User } from "./user.js" */

/** @type {Map<number, User>} */
const usersById = new Map();

Generics, casts and other features

JS utils.js
// @ts-check

/**
 * @template T
 * @param {T[]} items
 * @returns {T | undefined}
 */
export function last(items) {
  return items[items.length - 1];
}

const input = /** @type {HTMLInputElement} */ (document.querySelector("#email"));

/** @satisfies {Record<string, number>} */
const limits = { free: 3, pro: 50 };

@template declares a type parameter. A cast is written as a @type comment followed by an expression wrapped in parentheses — the parentheses are required. Most TypeScript features have a JSDoc equivalent.

When to choose JSDoc over .ts files

JSDoc in .js files.ts files
No build step — the file runs as-isNeeds a compile or strip step
Great for incremental adoptionConcise syntax, especially for complex types
Verbose for generics and advanced typesEvery TypeScript feature available directly
Used by some libraries (e.g. Svelte's source) to ship plain JSThe default choice for most new projects

Even with JSDoc, you can put complex types in a .d.ts or .ts file and import them into JavaScript via @import. That gives you the best of both: no build for the runtime code, full TypeScript syntax for the types.

Generating declarations from JavaScript

A JavaScript library documented with JSDoc can still ship .d.ts files for its TypeScript users:

$ terminal
npx tsc --allowJs --declaration --emitDeclarationOnly --outDir types src/index.js
Next: checking JavaScript with JSDoc is often the first step of a migration. The next lesson lays out a full plan for moving an existing JavaScript codebase to TypeScript.