TypeScript / THE COURSESearch
STAGE 04 / LIBRARIES & SCALE

LESSON 31 OF 36 · 24 MIN + PRACTICE

Migrating JavaScript safely

Adopt TypeScript a step at a time with checked JavaScript, JSDoc, and typed boundaries.

EXAMPLES CHECKED WITH TYPESCRIPT 7.0.2 / NODE 24

Where you are

You can maintain a strict TypeScript package. Existing JavaScript code may already deliver value and have useful tests. Migration should improve its contracts without simultaneously rewriting its behavior and architecture.

Mental model

Adoption is a sequence of smaller boundaries: check selected JavaScript, describe public contracts, validate inputs, convert owned modules, and tighten policy. A file extension change is not evidence that the program became safer.

JavaScript reality

The original runtime semantics remain. Closures, coercion, prototype mutation, and module loading do not disappear when a file becomes .ts. Preserve behavior with tests before changing annotations. Legacy patterns that rely on dynamic property creation may need explicit models, not merely renamed files.

TypeScript model

allowJs admits JavaScript files into a project. checkJs enables diagnostics in those files; per-file ts-check can start more narrowly. JSDoc expresses parameter types, returns, typedefs, imports, and generic relationships without changing executable syntax. TypeScript can generate declarations from checked JavaScript with declaration and emitDeclarationOnly.

TypeScript 7's JavaScript analysis differs from older Closure-style patterns. The current changes document lists unsupported or changed JSDoc forms. Prefer standard TypeScript-like JSDoc signatures and class syntax instead of assuming every historical annotation is still interpreted. Validate a representative legacy module before committing to a bulk migration tool.

Working example

Source: examples/valid/31.ts

TypeScript
export function normalizeLimit(value: number): number {
  if (!Number.isInteger(value) || value < 1) throw new Error("Invalid limit");
  return value;
}
console.log(normalizeLimit(2));
export {};

Verified runtime output:

text
2

This is the final TypeScript form of a small parsing boundary. The runnable JavaScript migration fixture in the labs uses JSDoc, checkJs, and declaration-only emit so you can compare before conversion.

A gradual configuration

Start with a bounded source directory and no accidental overwrite:

json
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "strict": true,
    "module": "NodeNext",
    "types": ["node"],
    "declaration": true,
    "emitDeclarationOnly": true,
    "rootDir": "src",
    "outDir": "types"
  },
  "include": ["src/**/*.js"]
}

Generated declarations belong in a separate output directory. Review them as public API, not as unquestionable truth about every dynamic runtime path.

Type-checker drill

Intentionally invalid: examples/invalid/31.ts

TypeScript
function double(value: number) {
  return value * 2;
}
double("2");
export {};

Actual TypeScript 7.0.2 diagnostic:

text
examples/invalid/31.ts(4,8): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

The migrated helper promises numeric input. Fix its callers or parse their text at the boundary. Broadening the function to any would preserve the original ambiguity instead of resolving it.

Runtime drill

Test the old and new functions with valid numbers, numeric strings, empty strings, and undefined. Identify any changed coercion behavior before calling it an improvement. Make the policy change explicit and version it when it affects external consumers.

Professional pattern

Migrate stable leaves and high-risk boundaries first, then work inward through dependents. Track intentional temporary escape hatches locally with owners and removal criteria. A temporary strictness compromise can be appropriate for a large migration, but do not present it as the finished course standard. Keep conversion commits small enough that behavior changes are reviewable.

Exercise

Take a small JavaScript module with tests. Add JSDoc and checkJs, emit declarations, and consume it from TypeScript. Fix one boundary bug separately from the mechanical conversion. Then rename the implementation to .ts and compare runtime tests and public declarations. Write a migration note explaining remaining dynamic assumptions and their owners.

Checkpoint

  • You can adopt checking before renaming every file.
  • You know allowJs and checkJs solve different problems.
  • You can emit declarations from JavaScript safely.
  • You separate behavior changes from conversion work.

Sources

JavaScript projects, declarations from JavaScript, and native compiler changes.

Check off this lesson when you can meet its checkpoint. Progress stays in this browser.