allowJs, checkJs and Leaving JSDoc Behind
See what allowJs and checkJs each do, move JSDoc types into TypeScript syntax and fix the JSDoc habits that TypeScript 7 rejects. After this, you can check JavaScript files on the way to converting them.
Course content
Sign in to save your progress.
-
Types That Compute
-
Conditional Types and infer Without Nested Nightmares
-
Template Literal Types for Routes and Event Names
-
-
-
Brands and Boundaries
-
Branded Types So IDs Don't Mix
-
satisfies and const Assertions When Inference Isn't Enough
-
-
-
Declare What JavaScript Didn't
-
Writing .d.ts for a Plain JS Module
-
Ambient Modules and Global Augmentation Without Breaking Everything
-
-
-
Projects That Scale
-
Project References and Solution-Style tsconfig
-
typescript-eslint Rules That Catch Real Mistakes
-
-
-
Migrate and Keep Moving
-
Move a JavaScript Folder to TypeScript Without a Big Bang
-
allowJs, checkJs and Leaving JSDoc Behind
-
Keep Build Times Sane: incremental and What Actually Slows tsc
-
-
- Final Check: TypeScript Advanced
Skip ahead: If you already know what allowJs and checkJs do and only want the part about removing JSDoc types, skip to "Leave the JSDoc behind", then continue.
A migration needs the compiler to accept two kinds of file at once. Two settings control that, and people mix them up because the names are close. allowJs decides whether JavaScript files are part of the program. checkJs decides whether the compiler reports errors in them. You can have the first without the second, and that combination is where most migrations start.
The earlier JavaScript Advanced lesson on @ts-check covers adding checks one file at a time. This lesson is about the project-wide settings, and about what happens to the JSDoc comments when a file becomes TypeScript.
The project
A small pricing module in JavaScript, typed with JSDoc, and a file that uses it. Everything runs as ES modules, and the compiler writes to dist.
{ "name": "pricing", "private": true, "type": "module" }
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"strict": true,
"allowJs": true,
"checkJs": false,
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
}
/**
* @typedef {Object} LineItem
* @property {string} name
* @property {number} price
* @property {number} [qty]
*/
/**
* Total of all lines, before tax.
* @param {LineItem[]} items
* @returns {number}
*/
export function subtotal(items) {
return items.reduce((sum, item) => sum + item.price * (item.qty ?? 1), 0);
}
/**
* @param {number} amount
* @param {number} [rate]
*/
export function withTax(amount, rate = 0.12) {
return Math.round(amount * (1 + rate) * 100) / 100;
}
import { subtotal, withTax } from "./pricing.js";
const items = [{ name: "pen", price: 12, qty: 3 }, { name: "pad", price: 40 }];
console.log(withTax(subtotal(items)));
console.log(withTax("100"));
allowJs alone: in the program, not checked
npx tsc -p .
echo "tsc exit code $?"
node dist/main.js
tsc exit code 0
85.12
112
The compiler read both files, wrote them to dist and said nothing. That is what allowJs buys you: you can build a mixed folder with one command, and TypeScript files can import JavaScript ones. The second line of output is the problem. withTax("100") passes a string to a function whose JSDoc says number, and the code happens to work because JavaScript turns "100" into a number when it multiplies. It would not work if the function added instead of multiplied.
checkJs: now the comments count
npx tsc -p . --checkJs
src/main.js(6,21): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
The JSDoc types are real types once checking is on. The same error you would get in a .ts file shows up in the JavaScript. This is where migrations stall: a repository that has never been checked has dozens of these, and turning checkJs on for everything blocks the build on the first day. Two ways to avoid that:
- Leave
checkJsoff and convert files to.tsone at a time. Only the converted files are checked. The cost is that the JavaScript is silent. - Turn
checkJson and put// @ts-nocheckat the top of every file that has errors you cannot fix yet. The cost is that you must remove those lines later, and a@ts-nocheckfile that nobody revisits is a hole.
I prefer the second one for a repository of any size, because the list of files that still have the comment is your migration to-do list, and grep can count it. The first leaves no list at all. This is a preference. A very small repository can do either.
Leave the JSDoc behind
When a file becomes .ts, its types move out of comments and into the syntax. The comment keeps the sentence for humans. Convert pricing.js.
export interface LineItem {
name: string;
price: number;
qty?: number;
}
/** Total of all lines, before tax. */
export function subtotal(items: LineItem[]): number {
return items.reduce((sum, item) => sum + item.price * (item.qty ?? 1), 0);
}
export function withTax(amount: number, rate = 0.12): number {
return Math.round(amount * (1 + rate) * 100) / 100;
}
The changes are mechanical:
@param {number} amountbecomesamount: number, and@param {number} [rate]becomes a default or a?.@returns {number}becomes: numberafter the parameter list.@typedefwith@propertylines becomes aninterfaceor atype.- The description stays as a normal comment. Keep it, and drop the lines that only repeated a type.
Remove the old file, fix the bug in main.js, and build.
import { subtotal, withTax } from "./pricing.js";
const items = [{ name: "pen", price: 12, qty: 3 }, { name: "pad", price: 40 }];
console.log(withTax(subtotal(items)));
console.log(withTax(100));
rm src/pricing.js
npx tsc -p . --checkJs
echo "tsc exit code $?"
node dist/main.js
tsc exit code 0
85.12
112
main.js still imports ./pricing.js. That path is the name of the output, so it works for the .ts file too, and main.js did not need to change. It now gets its types from pricing.ts instead of from comments.
💡 Tip: Delete the old .js file in the same commit that adds the .ts one. If both exist, the compiler uses the .ts file and leaves the .js one out of the program without a word. The old file then sits there looking real while nothing reads it, and a reviewer cannot tell which one counts.
TypeScript 7 reads JSDoc more strictly
TypeScript 7.0 reworked how it understands JavaScript. It follows the rules for .ts files more closely, and several old JSDoc habits stopped working. If your comments were written for the Closure compiler or for older TypeScript, expect a few errors when you upgrade. Three that I confirmed on 7.0.2:
/**
* @param {function(string): void} callback
*/
export function onDone(callback) {
callback("done");
}
/** @enum {string} */
export const Color = { Red: "red", Blue: "blue" };
/** @param {Color} color */
export function paint(color) {
return color;
}
/** @param {?} anything */
export function show(anything) {
return anything;
}
npx tsc -p . --checkJs
src/legacy.js(2,20): error TS1005: '}' expected.
src/legacy.js(11,13): error TS2749: 'Color' refers to a value, but is being used as a type here. Did you mean 'typeof Color'?
src/legacy.js(16,14): error TS1110: Type expected.
The first is the Closure function type. Write (value: string) => void instead. The second is @enum: the compiler no longer treats Color as a type, so you write typeof Color[keyof typeof Color] or a @typedef for the union. The third is a bare ?, which now needs to be any, written as the word. The TypeScript 7.0 announcement lists a few more, such as @class and a postfix !, which I did not test. Fix them in the JavaScript before you convert, and the later .ts files will have less to untangle.
Common mistakes
- Turning on
checkJsfor a repository that has never been checked, then adding// @ts-ignoreeverywhere to get green. That hides the errors you wanted to see. Use@ts-nocheckper file and count the files. - Leaving
@param {string}types in a.tsfile. TypeScript ignores them, and they drift away from the real types. - Converting a file and changing its behaviour in the same commit. Rename and add types first. Refactor afterwards.
If you skip this, you will either have a green build that checks nothing or a red one that nobody can merge. How many JavaScript files in your repository would pass checkJs today, and which one would you convert first?
About
See what allowJs and checkJs each do, move JSDoc types into TypeScript syntax and fix the JSDoc habits that TypeScript 7 rejects. After this, you can check JavaScript files on the way to converting them.
- Duration
- 10 min
- Last update
- 10/12/2026
Comments (0)
Sign in to commentNo comments yet.