Two tsconfig.json flags control how TypeScript treats your JavaScript files during migration: allowJs and checkJs. Most guides mention them in passing. This lesson goes deeper, because getting them right is what separates a smooth incremental migration from one that stalls because CI keeps breaking.
What allowJs does
allowJs: true tells the TypeScript compiler to include .js files in its compilation. Without it, the compiler ignores every file that hasn't been renamed to .ts. With it, .js and .ts files participate in the same build — they can import each other, they share type information, and they appear in the same output (if you're compiling to dist/).
This is what makes incremental migration possible. A converted .ts file can import an unconverted .js file, and the .js file can import a .ts file. The dependency graph doesn't need to be untangled before you can start.
// login-page.ts (converted) — imports from a .js file
import { getBaseUrl } from "./config.js"; // works with allowJs: true
// config.js (not yet converted) — imported by a .ts file
module.exports = { getBaseUrl: () => process.env.BASE_URL };What checkJs does
checkJs: true tells TypeScript to type-check .js files using whatever types it can infer — plus any JSDoc annotations you've added. Without checkJs, .js files compile but produce no errors. With it, TypeScript applies the same analysis to .js that it applies to .ts.
The practical difference:
// helper.js — with checkJs: true
/** @param {string} email @param {string} password */
function login(email, password) {
return cy.session([email, password], () => {
cy.visit("/login");
cy.get("#email").type(email);
cy.get("#password").type(password);
});
}
login(42, "password");
// Error: Argument of type 'number' is not assignable to parameter of type 'string'You get real type errors in .js files based on your JSDoc annotations — without renaming a single file.
The three useful combinations
| allowJs | checkJs | Effect |
|---|---|---|
true | false | JS files compile, no type errors reported — safe starting point |
true | true | JS files compile and are type-checked via inference and JSDoc |
false | n/a | Only .ts files compile — migration is complete |
The typical migration moves through these phases in order.
Per-file overrides
Even with checkJs: false at the project level, you can opt individual .js files into type checking with a single comment at the top:
// @ts-check
// TypeScript will now type-check this file using inference and JSDoc
/** @param {{ email: string; role: string }} user @returns {string} */
function formatUserLabel(user) {
return `${user.email} (${user.role})`;
}
formatUserLabel({ email: "alice@test.com" });
// Error: Argument of type '{ email: string; }' is not assignable.
// Property 'role' is missing.The inverse also exists:
// @ts-nocheck
// Skip type checking for this file even if checkJs: true is set globally
// Useful for legacy files with too many errors to fix right nowUse // @ts-check to opt files in one at a time before renaming them. Use // @ts-nocheck sparingly, with a TODO comment, on files where the error count is too high to address immediately.
Line-level suppression
For individual errors inside a // @ts-check file, TypeScript provides two comments:
// @ts-ignore ← suppresses the next line's error. No justification required.
const result = legacyHelper.doThing(data);
// @ts-expect-error ← suppresses the next line's error AND fails if there is no error.
// Prefer this — it tells you when the suppression is no longer needed.
const result = legacyHelper.doThing(data);// @ts-expect-error is the better suppressant during migration. When you later fix legacyHelper, TypeScript warns you that the suppression comment is now unnecessary — keeping the codebase clean.
A practical mid-migration Cypress project
Here's what a Cypress project looks like 40% through an incremental migration:
cypress/
├── e2e/
│ ├── login.cy.ts ← converted, fully typed
│ ├── checkout.cy.ts ← converted, fully typed
│ ├── search.cy.js ← @ts-check at top, JSDoc annotations
│ └── reports.cy.js ← not yet checked (allowJs only)
├── support/
│ ├── commands.ts ← converted
│ ├── e2e.ts ← converted
│ └── auth-helpers.js ← @ts-nocheck (complex legacy code)
└── fixtures/
└── users.json
This state is perfectly valid. The converted files get full TypeScript safety. The @ts-check files get partial safety via JSDoc. The remaining files compile but aren't checked. CI passes throughout.
Step 1 of 5
Phase 1: allowJs only
allowJs: true, checkJs: false. All .js files compile without errors. CI stays green. Baseline established.
JSDoc as a migration tool
JSDoc annotations let you type .js files precisely enough that TypeScript can verify callers:
// @ts-check
/**
* @typedef {{ id: string; email: string; role: "admin" | "member" }} TestUser
*/
/**
* @param {Partial<TestUser>} overrides
* @returns {TestUser}
*/
function createTestUser(overrides = {}) {
return {
id: crypto.randomUUID(),
email: "user@test.com",
role: "member",
...overrides,
};
}
createTestUser({ role: "superadmin" });
// Error: Type '"superadmin"' is not assignable to type '"admin" | "member"'This is practical for fixture factories and shared helpers that you want to type before renaming them. The annotations carry forward when you eventually rename the file to .ts — TypeScript ignores them (they're comments) and uses the explicit TypeScript annotations you add instead.
⚠️ Common mistakes
- Enabling
checkJs: trueproject-wide before the codebase is ready. This will surface hundreds of errors in files you haven't addressed yet, and will make CI red until you suppress them all. Use// @ts-checkper-file to control the rollout. - Using
// @ts-ignorewithout a comment explaining why. An unexplained suppression is technical debt. Future maintainers won't know if the error is still relevant. Write// @ts-ignore — LegacyPaymentHelper has no types yet, tracked in #123. - Forgetting that JSDoc types are not TypeScript types.
@param {string[]}in JSDoc is equivalent tostring[]in TypeScript, but JSDoc has gaps — complex generic types are verbose and sometimes incorrect. When a JSDoc annotation gets unwieldy, it's a signal that this file is ready to be renamed to.ts.
🎯 Practice task
Take the project from the previous lesson (with TypeScript installed and allowJs: true, checkJs: false).
- Pick the simplest JavaScript utility file in the project — a config file, a URL helper, or a data factory.
- Add
// @ts-checkat the top of the file. - Run
npm run type-check. Note which errors appear. - Add JSDoc annotations for the function parameters that have errors. Fix the errors.
- Add a deliberate mistake in a caller of one of the annotated functions — pass the wrong type. Confirm that
npm run type-checkcatches it. - Stretch: rename the file from
.jsto.ts. Remove the JSDoc annotations and replace them with TypeScript annotations directly on the function signatures. Runnpm run type-check. The same errors should be caught — now by TypeScript's native syntax instead of JSDoc.
The next lesson covers strictness settings — which flags to enable, in what order, and how to handle the errors each one surfaces.