Lesson 25 of 25
Migrating JavaScript to TypeScript
A realistic, incremental path for adding TypeScript to an existing codebase: allowJs, converting files gradually, JSDoc as a stepping stone, and the mistake that quietly undoes it.
Nothing about this track requires starting a project from scratch. A large share of real-world TypeScript adoption happens inside an existing JavaScript codebase, gradually — and doing that well is different enough from greenfield TypeScript to deserve its own lesson.
allowJs: letting the two coexist
The foundation of an incremental migration is a single tsconfig option:
{
"compilerOptions": {
"allowJs": true,
"checkJs": false
}
}allowJs lets .js files participate in the same project as .ts
files — imported from TypeScript, importing TypeScript, all in the same
build. With checkJs left off, existing JavaScript files are included but
not yet type-checked, which is exactly the low-risk starting point: nothing
about the existing code changes or newly breaks, but new .ts files can
already start being written and checked.
A realistic order of operations
Renaming every .js file to .ts in one sitting is rarely a good idea on
anything beyond a small project — it usually produces hundreds of errors at
once, most of them not really about bugs, just about newly-required
annotations, which is discouraging and hard to review as a single change.
A more realistic sequence:
- Enable
allowJs. Nothing breaks; new code can be written in TypeScript immediately. - Convert files with few dependents first — utility modules, small standalone components — rather than the file everything else imports. Converting a leaf file surfaces a manageable number of errors, contained to that one file, rather than cascading through everything downstream of it.
- Enable
checkJsonce enough of the codebase is either converted or ready to be checked, to start catching bugs in the JavaScript that remains, without needing to touch its syntax yet. - Turn on
strictlast, once the bulk of the migration is done — or incrementally, enabling one strict-family flag at a time (starting withnoImplicitAny) rather than all of them simultaneously, if the codebase is large enough that "all at once" would be unmanageable.
JSDoc: types without changing the file extension
A specific, genuinely useful stepping stone: with checkJs enabled,
TypeScript reads certain JSDoc comments as real type information, checked
exactly like a .ts annotation — without renaming the file at all:
// still a .js file
/**
* @param {string} name
* @param {number} age
* @returns {{ name: string, age: number }}
*/
function createUser(name, age) {
return { name, age };
}
createUser("Amara", "not a number");
// Argument of type 'string' is not assignable to parameter of type 'number'.This lets a team get real type checking on a file before committing to
converting its syntax — useful for files that are risky to touch broadly,
or for a team not yet ready to introduce .ts syntax project-wide but
still wanting the safety benefit.
Handling what's left: suppressions, used sparingly
Some errors surfacing during a migration genuinely need a temporary escape
hatch while the underlying issue gets scheduled separately. // @ts-expect-error
is the more disciplined version of this — unlike a plain // @ts-ignore, it
itself errors if the line it's attached to stops producing an error,
which is exactly the signal you want once the real fix eventually lands:
// @ts-expect-error — legacy call site, tracked in TICKET-123
const result = legacyFunction(wrongShapeArgument);Reach for this as a tracked, temporary marker — not a permanent substitute for fixing the underlying type.
The mistake that quietly undoes the benefit
as any is the single most common way a migration technically "succeeds" —
the file compiles, the count of .ts files goes up — while actually
delivering none of the safety this whole track has been about:
function processUser(data: any) {
return data.name.toUpperCase(); // no error here, ever, regardless of what data actually is
}Converting a file to .ts and immediately typing its trickiest parts any
gets the file extension changed without getting the actual benefit —
exactly the "any is contagious" problem from the unknown/any lesson,
just arriving through a migration instead of a fresh mistake. Prefer
unknown plus a real check, even if it takes a few more minutes per call
site than reaching for any — that extra time is, in a very real sense, the
entire point of doing the migration at all.
Try it yourself
What to remember
allowJslets JavaScript and TypeScript coexist in one project — the foundation of migrating gradually rather than all at once.- Convert files with few dependents first; enable
checkJsto catch bugs in remaining JavaScript before its syntax is converted. - JSDoc comments, checked via
checkJs, are a genuine stepping stone — real type checking without renaming a file yet. as anyduring a migration technically finishes the conversion while quietly discarding the safety it was meant to add — preferunknownand a real check instead.
Check yourself
4 questions · pass 3/4 to finish the course
1.What does the tsconfig option allowJs actually enable?
2.What does a JSDoc comment like
/** @param {string} name */achieve in an allowJs project with checkJs enabled?3.A team renames every .js file to .ts overnight and gets hundreds of errors. What is the recommended alternative?
4.What's the risk of reaching for
as anyto silence a type error during migration, versus fixing the underlying type?
4 left to answer