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
function applyDiscount(
price: number,
percent: number
): number {
return price - (price * percent) / 100;
}
/**
* @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.
/**
* @param {number} price
* @param {number} percent
* @returns {number}
*/
function applyDiscount(price, percent) {
return price - (price * percent) / 100;
}
applyDiscount(100, "10");
// no warning shown anywhere
// @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.
{
"compilerOptions": {
"checkJs": true,
"target": "es2020"
},
"exclude": ["node_modules"]
}
// 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.
interface User {
id: number;
name: string;
roles: string[];
}
function isAdmin(user: User): boolean {
return user.roles.includes("admin");
}
/**
* @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.