Migration Strategy
Migrate gradually, never all at once
Section titled “Migrate gradually, never all at once”A big-bang rewrite of a real JS codebase to TypeScript is how migrations die. The whole point of TypeScript is that it’s a gradual type system: JS and TS can coexist file by file, so you convert incrementally while shipping the whole time.
The phased path
Section titled “The phased path”flowchart LR js["allowJs: TS builds alongside JS"] --> checkjs["checkJs: type-check the JS too"] checkjs --> convert["rename .js to .ts file by file"] convert --> strict["ratchet strict flags one at a time"]
- Turn on
allowJs. Now TypeScript compiles a mixed project — your.jsfiles build unchanged, and you can start adding.tsfiles next to them. - Add
checkJs(and// @ts-check). TypeScript type-checks your JavaScript using inference and JSDoc. You get real errors before converting a single file — a free first pass. - Convert file by file. Rename
.jsto.ts, fix the errors that surface, commit. Start at the leaves (utilities with few dependencies) and work toward the core. - Ratchet strictness. Don’t start with full
strict. Turn on flags one at a time —noImplicitAny, thenstrictNullChecks, then the rest — fixing each wave before enabling the next.
Each step is shippable. You are never in a broken half-migrated state for long.
any is a tool, then a debt
Section titled “any is a tool, then a debt”During migration, any is a legitimate way to say “not yet typed — keep moving.” The danger is leaving it. Two disciplines keep it honest:
- Make it explicit and greppable. Prefer an explicit
any(or a// TODO: type thisalias) over an implicit one, so you can find and burn them down later. - Ratchet with
noImplicitAny. Once on, the compiler stops silently insertingany— new code must be typed, so the debt can only shrink.
@ts-expect-error beats @ts-ignore
Section titled “@ts-expect-error beats @ts-ignore”When you must suppress an error, use @ts-expect-error, not @ts-ignore:
// @ts-expect-error — legacy shape, tracked in TICKET-123legacyCall(weirdValue);Both silence the error on the next line, but @ts-expect-error also errors if the line stops having an error. So when you later fix the underlying type, the now-unnecessary suppression fails the build and tells you to remove it. @ts-ignore just rots silently.
Authoring libraries
Section titled “Authoring libraries”If you publish a package, a few extra rules apply:
- Ship declaration files (
.d.ts). Set"declaration": trueso consumers get your types. Point"types"inpackage.jsonat the entry.d.ts. - Don’t leak internal types. Only your public API’s types are a contract; keep implementation types un-exported so you can change them freely.
- Treat types as semver. A change that breaks consumers’ type checking is a breaking change, even if the runtime behavior is identical. Widening a parameter is usually safe; narrowing it, or changing a return type, can break builds downstream.