Lesson 2 of 5

JSDoc as a Partial Substitute

You can't get tsc back without a build step — but VS Code (and most modern editors) run a lighter version of the same type-checking engine over plain .js files, and it's driven by ordinary comments. Write your function signatures as JSDoc annotations instead of TypeScript syntax, and you get real autocomplete back, plus — with one extra line — a real subset of compile-time-style error checking, all without renaming a single file to .ts.

💡 The Bottom Line

A /** @param {type} name */ comment block above a function tells your editor (and, if you turn it on, the TypeScript compiler running in "check JS" mode) what shape to expect — no build step, no .ts extension, no separate compile output. It's not as strong as real TypeScript — there's no dedicated syntax for most advanced type features, and it relies on comments staying in sync with code by hand — but it recovers a meaningful chunk of what you just lost.

The same function, annotated two ways

TypeScript
function applyDiscount(
  price: number,
  percent: number
): number {
  return price - (price * percent) / 100;
}
Plain JS + JSDoc
/**
 * @param {number} price
 * @param {number} percent
 * @returns {number}
 */
function applyDiscount(price, percent) {
  return price - (price * percent) / 100;
}

📋 Key differences

  • JSDoc puts the types in a comment above the function, not inline in the signature — the function body itself is unchanged, plain JavaScript.
  • Editors like VS Code read these comments the same way they read TypeScript annotations for autocomplete and hover-documentation, with zero configuration required.
  • Without an extra opt-in, JSDoc comments are documentation and editor hints only — they don't block anything from running, even if the comment and the code disagree.

Turning hints into actual checking: @ts-check

A one-line comment at the very top of a .js file — // @ts-check — tells your editor's built-in TypeScript engine to actually type-check that file using its JSDoc comments as the source of truth, and underline real errors, not just offer autocomplete.

discount.js — hints only
/**
 * @param {number} price
 * @param {number} percent
 * @returns {number}
 */
function applyDiscount(price, percent) {
  return price - (price * percent) / 100;
}

applyDiscount(100, "10");
// no warning shown anywhere
discount.js — with @ts-check
// @ts-check

/**
 * @param {number} price
 * @param {number} percent
 * @returns {number}
 */
function applyDiscount(price, percent) {
  return price - (price * percent) / 100;
}

applyDiscount(100, "10");
// editor underlines this call:
// Argument of type 'string' is not
// assignable to parameter of type
// 'number'.

Project-wide: checkJs in jsconfig.json

Adding // @ts-check to every file one at a time works, but for a whole project it's usually turned on globally instead, via a jsconfig.json (or tsconfig.json with "allowJs": true) at the project root.

jsconfig.json
{
  "compilerOptions": {
    "checkJs": true,
    "target": "es2020"
  },
  "exclude": ["node_modules"]
}
Effect
// every .js file in the project
// now gets the same JSDoc-driven
// checking as if it had
// "@ts-check" at the top —
// no per-file opt-in needed

⚠️ Gotcha

JSDoc types are just comments — nothing forces them to stay accurate as the code around them changes. Rename a parameter, add a new one, or change what a function returns, and the @param/@returns tags happily keep saying the old thing unless a human remembers to update them. A stale JSDoc comment is worse than no comment at all: it actively tells your editor (and the next developer) something false with total confidence.

Describing object shapes with @typedef

For anything more complex than a primitive, @typedef gives you back something close to an interface — a named shape you can reuse across multiple JSDoc comments.

TypeScript
interface User {
  id: number;
  name: string;
  roles: string[];
}

function isAdmin(user: User): boolean {
  return user.roles.includes("admin");
}
Plain JS + JSDoc
/**
 * @typedef {Object} User
 * @property {number} id
 * @property {string} name
 * @property {string[]} roles
 */

/**
 * @param {User} user
 * @returns {boolean}
 */
function isAdmin(user) {
  return user.roles.includes("admin");
}

🔧 Try It Yourself

Open VS Code, create a plain .js file, add // @ts-check as the first line, and write a small function with a @param {number} annotation. Call it with a string argument and watch the red squiggly underline appear in your editor — no build, no tsc, no .ts file anywhere.

🙋 There Are No Dumb Questions

Q: If JSDoc plus @ts-check gets me real error checking, why would anyone bother migrating to actual TypeScript?
A: JSDoc covers the common cases — primitives, object shapes, arrays, unions written as @param {string|number} id — but plenty of TypeScript's more expressive features (generics with real inference, mapped/conditional types, discriminated unions with exhaustiveness checking) are clunky or impossible to express in a comment. JSDoc is a strong middle ground when a full TypeScript migration isn't practical, not a full replacement for it.

✅ Quick Check

A .js file has a JSDoc @param {number} price comment but no // @ts-check at the top. What happens if you call the function with a string?


Next up: JSDoc plus @ts-check covers a lot, but it's still opt-in and easy to skip entirely. The next lesson looks at defensive runtime checks — the pattern you fall back on when even JSDoc isn't in place.

← Back to
Next →