Skip to Content
JavaScript Advanced: Code That Holds Up in Production
Advanced
0 likes
Courses
JavaScript Advanced: Code That Holds Up in Production

JSDoc Types and // @ts-check Without Switching to TypeScript

Add types to plain JavaScript with JSDoc comments, turn on checking per file or per project, and run the checker in CI. After this, your editor flags the wrong argument before your customer does.

Course content

Sign in to save your progress.

Course Home

Skip ahead: If you already use JSDoc comments and only want the checker running in CI, skip to "Check the whole project".

TypeScript catches a whole class of bugs before you run anything: passing a string where a number belongs, misspelling a property, forgetting that map.get() can return undefined. You don't have to rewrite your project in TypeScript to get most of that. The TypeScript checker reads plain .js files, takes the types from JSDoc comments, and reports the same errors. No build step, no .ts files, and your code runs exactly as before.

One file, one line

Add // @ts-check as the first line of a file. Editors that use the TypeScript language service (VS Code does out of the box) start underlining problems right away. Then describe your types in JSDoc comments:

// @ts-check

/**
 * @typedef {object} CartItem
 * @property {string} sku
 * @property {number} price  Price in centavos.
 * @property {number} quantity
 */

/**
 * @param {CartItem[]} items
 * @param {number} [discountRate=0]  0.1 means 10% off.
 * @returns {number}
 */
export function cartTotal(items, discountRate = 0) {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  return Math.round(subtotal * (1 - discountRate));
}

cartTotal([{ sku: "MOUSE-01", price: "450", quantity: 2 }]);
cartTotal([{ sku: "CABLE-02", price: 150, qty: 1 }]);
cartTotal([], "10%");

Run the checker from the command line (install it once with npm install --save-dev typescript):

npx tsc --noEmit --allowJs --target es2022 --module nodenext cart.js
cart.js(20,31): error TS2322: Type 'string' is not assignable to type 'number'.
cart.js(21,43): error TS2353: Object literal may only specify known properties, and 'qty' does not exist in type 'CartItem'.
cart.js(22,15): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

All three calls run without any error in plain JavaScript. The first one even gives a believable answer, because "450" * 2 is 900. The second gives NaN. The third gives NaN. None of them crash, which is exactly why they reach production.

The JSDoc you'll actually use

  • @param {string} name and @returns {number} for functions.
  • @param {number} [limit=20] for an optional parameter with a default.
  • @typedef {object} Name with @property lines for an object shape you reuse.
  • @type {Map<string, Order>} on a variable.
  • Unions: {"draft" | "paid" | "refunded"} limits a string to those values.
  • {import("./types.js").Order} to use a type from another file.

You don't need to type everything. The checker infers most local variables. Type the edges: function parameters, return values of exported functions, and data that comes from outside.

Check the whole project

Per-file // @ts-check is a good start on an old project. For a new one, turn it on everywhere with a tsconfig.json (or jsconfig.json, which editors also read):

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext"
  },
  "include": ["src/**/*.js"]
}

noEmit means "check only, don't write files". strict turns on the checks that matter most, including null checks. Now plain npx tsc checks everything under src:

/** @type {Map<string, { id: string, total: number }>} */
const orders = new Map();

/** @param {string} id */
export function orderTotal(id) {
  const order = orders.get(id);
  return order.total;
}
src/orders.js(7,10): error TS18048: 'order' is possibly 'undefined'.

That's the checker telling you what happens when someone asks for an order that doesn't exist. Add if (!order) throw new Error("Unknown order " + id); before the return, run npx tsc again, and it passes with exit code 0.

Add it to package.json and to CI, next to your tests:

"scripts": {
  "typecheck": "tsc",
  "test": "vitest run"
}

💡 Tip: Turning on strict in a big old project can produce hundreds of errors. Start with checkJs and strict: false, fix those, then turn on strictNullChecks alone. Null checks find the most real bugs per hour of work.

Silencing, properly

Sometimes the checker is wrong, or the fix isn't worth it today. Put // @ts-expect-error with a reason on the line above. Unlike // @ts-ignore, it fails when the error goes away, so the comment doesn't outlive the problem. Use // @ts-nocheck at the top of a file to leave a whole file out while you work on others.

⚠ Warning: JSDoc types are only checked, never enforced at runtime. Data from an API, a form or a file can still be anything. Validate it where it enters your program, then let the types cover the rest.

When to switch to real TypeScript

JSDoc gets clumsy with complex generic types, and some teams find .ts syntax easier to read. If most of your time goes into writing types, a TypeScript project with a build step is worth it, and it's a course of its own. For a lot of Node services and small libraries, JSDoc plus tsc --noEmit is enough, and I'd start there.

Common mistakes

  • Adding JSDoc but never running the checker, so the comments drift from the code.
  • @ts-ignore everywhere instead of @ts-expect-error with a reason.
  • Trusting types on data that came from outside.
  • Turning on strict across 300 files on day one and giving up.

What happens if you skip this

You'll keep finding wrong arguments and misspelled properties the way you find them now: when a customer reports a NaN on their invoice.

In the cartTotal example, which of the three bad calls would your current tests have caught, and which would have reached a customer?

About

Add types to plain JavaScript with JSDoc comments, turn on checking per file or per project, and run the checker in CI. After this, your editor flags the wrong argument before your customer does.

Duration
5 min
Last update
08/28/2026
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.