# How to Migrate a JavaScript Codebase to TypeScript Incrementally

> A practical, step-by-step guide to migrating JavaScript to TypeScript without stopping feature work or drowning in compiler errors. 700+ words.

- **Published:** 2026-01-03
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** typescript, javascript, migration, production
- **Canonical URL:** https://clixo.sh/blog/how-to-migrate-javascript-to-typescript-incrementally

You have a working JavaScript codebase. Maybe it is 20 files, maybe 200. Either way, you have heard the pitch for TypeScript and decided it is worth doing — but a full rewrite is not realistic. You need to ship features while the migration happens around you.

The good news: TypeScript was designed for exactly this scenario. The bad news: most teams attempt too much at once, get buried in hundreds of compiler errors, and quietly revert their changes two weeks later. Here is how to do it right.

## The Core Strategy: Migrate JavaScript to TypeScript File by File

TypeScript's `allowJs` flag lets you mix `.js` and `.ts` files in the same project. That is your escape hatch. You never have to migrate everything at once.

**Start with the tsconfig, not the files.**

Create a `tsconfig.json` at the root with these settings:

```json
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "strict": false,
    "outDir": "dist",
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext"
  },
  "include": ["src"]
}
```

`allowJs` lets TypeScript compile your existing `.js` files. `checkJs: false` means it will not yell at your old JavaScript yet. `strict: false` gives you room to migrate without enabling every flag on day one. This configuration should produce zero new errors.

Run `tsc --noEmit`. If it fails, fix those issues before touching a single file.

```mermaid
flowchart TD
  A["tsconfig.json with allowJs"] --> B["Phase 1: Migrate leaf files"]
  B --> C["Utility functions"]
  B --> D["Type definitions"]
  C --> E["Phase 2: Service modules and controllers"]
  D --> E
  E --> F["Entry points last"]
  F --> G["Phase 3: Tighten config flags one by one"]
  G --> H["Remove allowJs — full TypeScript"]
```

### Phase 1: Migrate the Leaves First

Your codebase has a dependency graph. Utility functions and pure helpers have no internal imports — they are the leaves. Start there. Rename `utils/format.js` to `utils/format.ts`. Add types to the function signatures. Fix the errors TypeScript surfaces. Commit.

Do not touch files that import from many places yet. If you rename `db.js` to `db.ts` on day one, you will face cascading errors across every file that imports it.

Work inward from the edges toward the entry points. A good order:

1. Pure utility functions
2. Type definition files (create a `types/` directory with `.ts` files)
3. Service modules with narrow dependencies
4. Route handlers and controllers
5. Entry points last

### Phase 2: Tighten the Config Gradually

Once 70-80% of your files are `.ts`, you can start tightening. Flip these flags one at a time and fix the errors each one surfaces:

- `"strict": true` — enables `strictNullChecks`, `noImplicitAny`, and six others
- `"noUncheckedIndexedAccess": true` — catches array/record access that can return `undefined`
- `"noImplicitReturns": true` — ensures every code path in a function returns a value
- `"checkJs": true` — starts checking any remaining `.js` files

Enable one flag per PR. Review what breaks. Fix it. Merge. Repeat.

### Phase 3: Delete the allowJs Escape Hatch

Once every file is `.ts`, remove `allowJs` and `checkJs`. Set `strict: true` if you have not already. Run `tsc --noEmit` in CI on every push so regressions are caught before they reach main.

## Common Pitfalls That Slow Teams Down

**Using `as` to silence errors.** Type assertions like `const user = data as User` feel like fixes but they are not. They tell TypeScript to trust you, and TypeScript will. If the runtime shape does not match, you get a crash with no warning. Use runtime validation (Zod, Valibot) at trust boundaries instead.

**Enabling strict on an unmigrated codebase.** If you flip `strict: true` before renaming any files, you will see thousands of errors in your JavaScript. TypeScript cannot infer types it does not have. Gradual strictness is the point.

**Migrating shared packages last.** If your project is a monorepo, migrate shared packages — utilities, API clients, shared types — before the applications that consume them. A typed package gives you accurate inference across the board.

**Forgetting to type external data.** TypeScript types are compile-time constructs. JSON from an API, data from a database, environment variables — these are untyped at runtime regardless of what TypeScript thinks. Add schema validation at every ingestion point.

## How Long Does It Take

A 10,000-line codebase with one focused engineer typically reaches full `.ts` coverage in two to four weeks when done incrementally. The first week is the tsconfig setup, leaf migration, and building the muscle. Weeks two and three are the middle of the graph. The final push is usually the cleanest.

The milestone that matters most is not "all files renamed" — it is `tsc --noEmit` running green in CI. Everything before that is preparation.

---

If you are building a TypeScript-first product from scratch or need expert help on a migration that has stalled, [talk to Clixo](https://clixo.sh/#contact). We build production systems and know where the traps are.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
