JSDoc is flexible enough that two people can type the same function in five different ways, and all five pass tsc. That is fine for one file and tiring across a codebase. These are the house rules this site follows in its own examples; each one makes the code easier to read or keeps the types working in more TypeScript versions. Adopt them as a set, or argue with them in your own style guide.
One comment style
Every exported function, class and constant gets one /** */ comment directly above it. The comment opens with a sentence that says what the thing does, then a blank line, then the tags. Parameters are written @param {Type} name - What it is, with the hyphen, so editors show the description cleanly. Optional parameters go in brackets.
/**
* Works out the shipping cost of an order. Orders at or above the free
* threshold ship for nothing.
*
* @param {number} subtotal - The order total before shipping, in cents.
* @param {"standard" | "express"} [speed] - Defaults to "standard".
* @returns {number} The shipping cost in cents.
*/
export function shippingCost(subtotal, speed = "standard") {
if (subtotal >= 5000) return 0;
return speed === "express" ? 1500 : 500;
}/**
* Works out the shipping cost of an order. Orders at or above the free
* threshold ship for nothing.
*
* @param subtotal - The order total before shipping, in cents.
* @param speed - Defaults to "standard".
* @returns The shipping cost in cents.
*/
export function shippingCost(subtotal: number, speed: "standard" | "express" = "standard"): number {
if (subtotal >= 5000) return 0;
return speed === "express" ? 1500 : 500;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} The TypeScript equivalent tab shows why the description belongs in the comment and the types in the braces: a .ts file keeps the same comment for the reader and moves the types into the code. When a parameter has a default value in the code, as speed does here, the brackets only say it is optional; the default comes from the code.
Write @returns when the function is exported or when the return type is not obvious from one glance at the body. Inside a module, inferred return types are fine.
TypeScript syntax inside the braces
Inside the braces, write what you would write after a colon in a .ts file: string[] rather than Array.<string>, Record<string, number> rather than Object.<string, number>, (item: Item) => boolean rather than function(Item): boolean, and string | null rather than ?string. TypeScript 5.9 still accepts the older Closure forms, but TypeScript 7.0 no longer parses most of them, and readers who know TypeScript read the new forms faster. The Closure syntax page has the full mapping.
Use the lowercase primitives (string, number, boolean, object). Object with a capital O means almost anything, and {} means any value except null and undefined; object types explains the difference.
Where @typedefs live
A type used by one module lives at the top of that module. A type used by several modules lives in one types module, imported where it is needed:
/**
* @typedef {object} LineItem
* @property {string} sku
* @property {number} quantity
* @property {number} unitPrice - In cents.
*/
/**
* @typedef {object} Cart
* @property {string} id
* @property {LineItem[]} items
*/
export {};jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} /** @import { Cart, LineItem } from "./types.js" */
/**
* @param {Cart} cart
* @param {LineItem} item
* @returns {Cart}
*/
export function addItem(cart, item) {
return { ...cart, items: [...cart.items, item] };
}
/** @param {Cart} cart */
export function cartTotal(cart) {
return cart.items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0);
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} The export {} line makes types.js an ES module, which matters: a @typedef in a module is exported, while a @typedef in a file with no import or export is global to the whole project, where names collide easily. Give each area of the code its own types module (cart/types.js, billing/types.js) rather than one file for everything.
Use a .d.ts file only for what JSDoc cannot express: global declarations, augmenting another package’s types, and declarations for an untyped dependency. The recipes on globals and untyped packages show both.
Naming
- Types are nouns in PascalCase:
LineItem,Route,CheckoutResult. NoIorTprefix: in JavaScript there is no interface keyword to tell apart, and the comment already says it is a type. - Type parameters are a single capital letter when there is one obvious role (
Tfor the item,Kfor a key), and a short word when there are several:@template Item, Key. - Callback types are named for their job:
OnChange,Comparator,Reducer. The@callbacktag is the right place for one that is reused; a one-off goes inline as an arrow type. - A type that describes a value’s shape should match the value’s name where both exist:
configandConfig.
@import, not inline import types
Since TypeScript 5.5, @import brings types from another module into scope with the syntax of an import statement, and it disappears at runtime. Put one @import comment at the top of the file, under the real imports, rather than writing import("./types.js").Cart in every tag. It reads like the rest of the module, and the names stay short.
Because @import is a comment, it never loads anything when the code runs, so it cannot create an import cycle at runtime. Two things to know: a name in @import that you never use is not checked at all, so a typo there stays hidden until you use it; and the inline import() form is still the one to use in a .d.ts file or in a one-off place.
@satisfies rather than a cast
A cast, /** @type {Settings} */ (value), tells TypeScript to trust you: an object missing a required property passes. @satisfies checks the value against the type without changing the value’s own type, so the object keeps its specific keys:
/** @typedef {{ path: string, auth: boolean }} Route */
/** @satisfies {Record<string, Route>} */
export const routes = {
home: { path: "/", auth: false },
account: { path: "/account", auth: true },
admin: { path: "/admin", auth: true, role: "owner" },error TS2353: Object literal may only specify known properties, and 'role' does not exist in type 'Route'.
};
export const accountPath = routes.acount.path;error TS2551: Property 'acount' does not exist on type '{ home: { path: string; auth: false; }; account: { path: string; auth: true; }; admin: { path: string; auth: true; role: string; }; }'. Did you mean 'account'?jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} With @type {Record<string, Route>} instead, the misspelled routes.acount would be accepted, because any string key would be allowed. Keep casts for the one job they are good at, telling TypeScript something it cannot know, such as which element type querySelector found. When you do cast, keep the parentheses around the value: they are what makes the comment a cast.
Keep comments next to the code they type
A JSDoc comment belongs to the declaration directly after it. Move a line between them and the comment types the wrong thing, silently:
/** @param {number} cents */
const CURRENCY = "USD";
export function formatCents(cents) {error TS7006: Parameter 'cents' implicitly has an 'any' type.
return `${(cents / 100).toFixed(2)} ${CURRENCY}`;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} The same rule bites in two other ways: only the last /** */ comment above a declaration is read, and a comment on the same line as a class’s opening brace is not attached to the member after it. Write the tags and the description in one comment, on their own lines. How TypeScript reads a JavaScript file has examples of both.
Prettier and JSDoc
Prettier and checked JavaScript get along without special settings:
- Prettier does not rewrap or reformat the text of comments. Per its rationale page, it leaves comments as they are and only fixes the indentation of JSDoc-style blocks (every line starting with
*). - Casts keep their parentheses. Per the Prettier 2.0 announcement, it keeps the parentheses around
/** @type {T} */ (value)with its default parser for JavaScript files (babel); the same announcement says theflowandtypescriptparsers do not recognize these casts. So do not set"parser": "typescript"for.jsfiles in your Prettier config. - A community plugin,
prettier-plugin-jsdoc, does reformat comment blocks. It is not part of Prettier; try it on a branch and run tsc afterwards before adopting it.
For linting the comments themselves (a missing @param, a tag name that does not exist), eslint-plugin-jsdoc has a configuration named flat/recommended-typescript-flavor, which its README describes as meant for JavaScript projects that write TypeScript-flavored JSDoc. tsc already reports a @param whose name matches no parameter (TS8024), so the linter’s value is in the rules tsc does not have, such as requiring a description.
Every example on this page was checked with TypeScript 5.9.
Questions
Should types live in .d.ts files or in JSDoc?
In JSDoc, next to the code, for everything JSDoc can say. Use a .d.ts file for what it cannot: global declarations, module augmentation, and types for a package that has none.
Does Prettier change JSDoc comments?
Prettier leaves the text of comments as it is and only fixes the indentation of JSDoc-style blocks. Since Prettier 2.0 it keeps the parentheses a JSDoc cast needs, with its default parser for .js files.
Is Array.<string> wrong?
TypeScript 5.9 accepts it, but TypeScript 7.0 drops most of the older Closure forms. Write string[] and arrow function types; they mean the same thing in every version.
Sources
- JSDoc Reference · TypeScript handbook
- TypeScript 5.5: the @import tag · TypeScript handbook
- Rationale: comments · Prettier
- Prettier 2.0: Closure-style type casts · Prettier blog
- eslint-plugin-jsdoc · GitHub
Last reviewed 21 October 2026.