Skip to Content
TypeScript Advanced: Types That Hold Up in Production
Advanced
0 likes
Courses
TypeScript Advanced: Types That Hold Up in Production

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.

Course Home

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 checkJs off and convert files to .ts one at a time. Only the converted files are checked. The cost is that the JavaScript is silent.
  • Turn checkJs on and put // @ts-nocheck at 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-nocheck file 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} amount becomes amount: number, and @param {number} [rate] becomes a default or a ?.
  • @returns {number} becomes : number after the parameter list.
  • @typedef with @property lines becomes an interface or a type.
  • 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 checkJs for a repository that has never been checked, then adding // @ts-ignore everywhere to get green. That hides the errors you wanted to see. Use @ts-nocheck per file and count the files.
  • Leaving @param {string} types in a .ts file. 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
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.