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:
// @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):
{
"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:
// @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:
// @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+):
// @ts-check /** @import { User } from "./user.js" */ /** @type {Map<number, User>} */ const usersById = new Map();
Generics, casts and other features
// @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-is | Needs a compile or strip step |
| Great for incremental adoption | Concise syntax, especially for complex types |
| Verbose for generics and advanced types | Every TypeScript feature available directly |
| Used by some libraries (e.g. Svelte's source) to ship plain JS | The 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:
npx tsc --allowJs --declaration --emitDeclarationOnly --outDir types src/index.js