# Migrate JavaScript to TypeScript with AI: a safe plan

URL: https://agentvera.dev/en/blog/migrate-javascript-to-typescript-with-ai/ · Published: 2026-10-27

How to migrate JavaScript to TypeScript with AI agents: a tsconfig that accepts JS, file-by-file order, a strictness ramp, an any budget and tsc as the verify step.

**In short:**

- Start with a tsconfig that accepts JavaScript (allowJs, strict set to false on purpose), so the app keeps building while agents convert files one at a time, leaves first.
- Give every agent the same definition of done: tsc passes, tests pass, no @ts-ignore, and the count of explicit any stays inside a budget that only goes down.
- Turn on strict flags one by one, and split the work across parallel agents by directory in separate worktrees, with shared types and config owned by one agent only.

To migrate JavaScript to TypeScript with AI, you don’t ask an agent to “convert the project”. You set up a TypeScript config that accepts the JavaScript you already have, then let agents convert files in a fixed order, with the type checker and the tests deciding when each step is done. The compiler is the best reviewer you’ll ever give an agent: it’s fast, it never gets tired and it can’t be talked into “looks fine”.

This guide is the hands-on version of that idea: the exact `tsconfig.json` to start with, how to pick the order of files, how to tighten strictness without a week of red builds, how to keep `any` under control, how to make type checking the agent’s own verify step and how to split the work across parallel agents without merge conflicts. For the general safety net of a legacy refactor (characterization tests, rollback, review), read [refactor legacy code with AI](/en/blog/refactor-legacy-code-with-ai/ "Refactor legacy code with AI") first; this post doesn’t repeat it.

Version-sensitive details here follow the official TypeScript documentation as of October 2026.

## Why agents are good at this job

A JS to TS migration is mostly mechanical work with a clear signal at the end of each step. That is exactly what coding agents are good at:

- **The feedback is instant.** `tsc` prints file, line and error code. An agent can read that and fix it without guessing.
- **The work splits naturally.** Files and folders can be converted one at a time, so the job breaks into many small, reviewable steps.
- **The boring part is huge.** Adding parameter types, writing interfaces for objects that already exist and installing `@types` packages is the kind of work people put off for years.

The risk is just as clear. An agent under pressure to make the compiler green will reach for `any`, `as` casts and `@ts-ignore`, because they make errors disappear. Most of this guide is about closing those exits.

## Step 1: a tsconfig that accepts your JavaScript

The first commit should change no application code. It adds TypeScript and a config that type checks the project while every file is still `.js`.

```bash
npm install --save-dev typescript @types/node
```

```json
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "strict": false,
    "noEmit": true,
    "incremental": true,
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "types": ["node"],
    "skipLibCheck": true
  },
  "include": ["src"]
}
```

What each choice does:

- **`allowJs`** lets `.js` and `.ts` files live in the same program, so a `.ts` file can import a `.js` file and the other way around.
- **`checkJs: false`** keeps TypeScript from reporting errors in files you haven’t touched yet. You’ll opt files in one by one.
- **`strict: false`** is deliberate. Since TypeScript 6.0, `strict` defaults to `true`, so leaving it out would switch on every strict check on day one.
- **`types: ["node"]`** matters for the same reason: in TypeScript 6.0 the default `types` is an empty array, so Node globals aren’t picked up unless you list them. Add `"jest"` or `"vitest/globals"` if your tests rely on globals.
- **`noEmit`** fits projects where a bundler, `tsx` or a framework already turns code into JavaScript and TypeScript only checks it. If `tsc` builds your app, drop it and set `outDir` instead.
- **`module` and `moduleResolution`**: use `nodenext` for Node apps; for code that a bundler builds, `"module": "esnext"` with `"moduleResolution": "bundler"` is usually the right pair.

Add one script and make CI run it:

```json
{
  "scripts": {
    "typecheck": "tsc -p tsconfig.json"
  }
}
```

Run it once. On a JavaScript codebase with `checkJs` off, it should pass or show only a few config errors. Fix those, commit, and you have a baseline. From here on, “the build is green” means `npm run typecheck` and `npm test` both pass.

## Step 2: preview errors with @ts-check before renaming

Before a file is renamed, you can see what TypeScript thinks of it. Add `// @ts-check` as the first line of a `.js` file and TypeScript reports errors in it, using JSDoc comments as types:

```js
// @ts-check

/**
 * @param {number} cents
 * @param {string} currency
 * @returns {string}
 */
export function formatPrice(cents, currency) {
  return new Intl.NumberFormat('en', { style: 'currency', currency }).format(cents / 100);
}
```

This is a good first job for an agent on a large file: “Add `// @ts-check` to `src/orders/pricing.js`, fix the errors with JSDoc types only, don’t change behaviour.” You get type safety without a rename, and the later conversion becomes almost mechanical. If you prefer the opposite approach, set `checkJs: true` and put `// @ts-nocheck` on the files that aren’t ready yet; then removing that line is the step.

## Step 3: pick the order, leaves first

Convert files that import nothing from your own code first, then the files that only import those, and so on up to the entry points. That way every file you convert can lean on types that already exist.

You don’t have to work the order out by hand. [madge](https://github.com/pahen/madge "madge") reads the import graph:

```bash
npx madge --leaves --extensions js,jsx src/
npx madge --circular --extensions js,jsx src/
```

`--leaves` lists modules with no dependencies, and `--circular` lists import cycles. Cycles are worth knowing about up front: files in a cycle have to be converted together, so plan them as one step.

A practical batching rule: one step is either one large file or one folder of small files, small enough that the diff fits in one sitting of review. Put the list in a checked-in file such as `MIGRATION.md`, one line per step, and let agents tick it off. That file becomes the shared plan for every agent and every session.

## Step 4: one conversion prompt, one definition of done

Use the same prompt shape for every step, so agents don’t invent their own rules:

```text
Convert src/billing/ from JavaScript to TypeScript.

- Rename with git mv so history follows the files (.js -> .ts, .jsx -> .tsx).
- Add types for parameters, return values and exported objects.
  Prefer types inferred from real usage over wide ones.
- Don't change runtime behaviour. No refactoring, no renaming,
  no "while I'm here" fixes. If you find a bug, note it in
  MIGRATION.md under "Found during migration" and leave it.
- No @ts-ignore, no @ts-nocheck. @ts-expect-error only with a reason
  on the same line. No new `any` beyond the budget in .any-budget.
- Don't edit tsconfig.json, package.json or src/types/.
  If you need a change there, stop and tell me what and why.

Done means: npm run typecheck passes, npm test passes,
npm run lint passes, and MIGRATION.md is updated.
```

Two lines in that prompt do most of the work. “Don’t change runtime behaviour” keeps the diff reviewable, because a pure type migration should only add annotations, renames and imports. “Don’t edit tsconfig.json” stops the cheapest way to green: loosening the compiler.

Put the stable parts of this prompt in `CLAUDE.md` or `AGENTS.md` so every session starts with them; [CLAUDE.md and AGENTS.md](/en/blog/claude-md-agents-md/ "CLAUDE.md and AGENTS.md") explains how to share one set of rules between agents.

## Step 5: an any budget that only goes down

You won’t get to zero `any` in one pass, and banning it outright tends to produce worse escapes, like `as unknown as Foo`. A budget is more honest: you allow a number, and the number only goes down.

First make `any` and suppression comments visible with typescript-eslint:

```js
// eslint.config.js (excerpt)
rules: {
  '@typescript-eslint/no-explicit-any': 'warn',
  '@typescript-eslint/ban-ts-comment': ['error', {
    'ts-expect-error': 'allow-with-description',
    'ts-ignore': true,
    'ts-nocheck': true,
    minimumDescriptionLength: 10,
  }],
}
```

`ban-ts-comment` with these options rejects `@ts-ignore` and `@ts-nocheck` and only accepts `@ts-expect-error` with a written reason of at least ten characters. `@ts-expect-error` is the better escape anyway: once the error is fixed, the comment itself becomes an error, so it can’t go stale.

Then count explicit `any` and compare it with a number stored in the repo:

```bash
#!/usr/bin/env bash
# scripts/any-budget.sh
count=$(npx eslint src --format json \
  | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const r=JSON.parse(s);console.log(r.flatMap(f=>f.messages).filter(m=>m.ruleId==="@typescript-eslint/no-explicit-any").length)})')
budget=$(cat .any-budget)
echo "explicit any: $count (budget $budget)"
[ "$count" -le "$budget" ] || { echo "any budget exceeded"; exit 1; }
```

Run it in CI and in the agent’s definition of done. When a step brings the count down, lower `.any-budget` in the same commit. Tell the agent where the budget lives; agents respect a number much better than “avoid any where possible”.

Watch for the other escapes, which the count doesn’t catch: `as` casts on values that came from outside (`JSON.parse`, request bodies), `Function` and `object` as parameter types, and a `declare module '*'` line that turns every import into `any`. Ask the reviewer, human or agent, to list every new cast in the diff.

## Step 6: make type checking the agent’s verify step

An agent that says “done” without running `tsc` is guessing. Make the check part of how the agent ends a turn, not something you remember to ask for.

In Claude Code, a Stop hook can refuse to let the turn end while the type check fails. Save this as `.claude/hooks/typecheck.sh`:

```bash
#!/usr/bin/env bash
# Exit 2 sends stderr back to Claude and keeps it working.
input=$(cat)
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0   # already retried once; let the turn end
fi
npx tsc -p tsconfig.json --pretty false 1>&2 || exit 2
```

and register it in `.claude/settings.json`:

```json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": ".claude/hooks/typecheck.sh" }
        ]
      }
    ]
  }
}
```

When a Stop hook exits with code 2, Claude Code doesn’t stop and shows the hook’s stderr to Claude, so the agent sees the compiler errors and keeps fixing. The `stop_hook_active` check lets the turn end on the second try instead of looping. The [Claude Code hooks guide](/en/blog/claude-code-hooks-guide/ "Claude Code hooks guide") covers hooks in more depth. A `PostToolUse` hook on `Edit|Write` can run a faster check after every edit, though on large projects a full `tsc` per edit gets slow.

Codex and Gemini CLI don’t read Claude’s hooks, so put the same rule in `AGENTS.md` or `GEMINI.md`: “Before you say a task is done, run `npm run typecheck` and `npm test` and paste the last lines of the output.” Asking for the output is what makes the instruction stick.

## Step 7: ramp up strictness one flag at a time

Once every file is `.ts`, or one area is, turn the strict checks on in this order, with a separate commit and agent task for each:

1. **`noImplicitAny`**: every parameter and variable must have a known type. This produces the most errors and the most useful types.
2. **`strictNullChecks`**: `null` and `undefined` become their own types. This one finds real bugs, the “cannot read property of undefined” kind.
3. **The rest of the strict family**: `strictFunctionTypes`, `strictBindCallApply`, `strictPropertyInitialization`, `noImplicitThis`, `useUnknownInCatchVariables`. Usually fewer errors each.
4. **`"strict": true`** replaces the individual flags once they all pass.
5. **Optional extras** that aren’t part of `strict`: `noUncheckedIndexedAccess` (indexed reads may be `undefined`) and `exactOptionalPropertyTypes`.

You don’t have to flip a flag for the whole repo at once. A second config that extends the first and covers only finished folders lets strict code stay strict while the rest catches up:

```json
{
  "extends": "./tsconfig.json",
  "compilerOptions": { "strict": true },
  "include": ["src/billing", "src/utils"]
}
```

Save it as `tsconfig.strict.json` and run `tsc -p tsconfig.strict.json` in CI next to the main check. Each time an agent finishes a folder, it adds the folder to `include`. When `include` covers `src`, move `strict: true` into the main config and delete the second file.

A caution for `strictNullChecks`: the fastest fix for “possibly undefined” is the non-null assertion `!`. A few are fine; dozens mean the agent is hiding the problem. Ask for a guard, a default or a type change instead, and treat new `!` like new `any` during review.

## Step 8: split the work across parallel agents

A migration is one of the few jobs where running several agents at once really pays off, because folders are largely independent. The way to make it work is to split by directory and give each agent its own [git worktree](/en/glossary/git-worktree/ "What is a git worktree"):

```bash
git worktree add ../app-ts-billing -b ts/billing
git worktree add ../app-ts-orders  -b ts/orders
git worktree add ../app-ts-users   -b ts/users
```

Start one agent in each folder with the step prompt from above, pointed at its directory. A few rules keep the branches from colliding:

- **Shared files have one owner.** `tsconfig.json`, `package.json`, the lockfile, `eslint.config.js` and shared declarations in `src/types/` change only on the main branch, by you or by one coordinating agent. Folder agents request changes there instead of making them.
- **Shared types go first.** If several folders need the same `User` or `Order` type, make that a sequential step before the parallel ones start. Otherwise three agents write three slightly different `User` interfaces.
- **Install `@types` packages centrally.** Two branches that each add a different package rewrite the lockfile and conflict every time.
- **Don’t parallelize a cycle.** Folders that import each other in a circle belong to one agent.
- **Merge one branch at a time and type check after each merge.** Two branches can each pass `tsc` and fail together, for example when one changes an exported type the other relies on. Rebase the next branch on the new main before merging it.

[Git worktrees for parallel agents](/en/blog/git-worktree-parallel-agents/ "Git worktrees for parallel agents") covers setup and cleanup in detail. Three or four agents is a sensible start; beyond that, reviewing the branches usually becomes the bottleneck, not writing them.

## Step 9: let the tests prove nothing changed

Types tell you the code is consistent with itself. Only tests tell you it still does what it did. During a migration:

- **Run the full suite after each step**, not just the converted folder’s tests. A changed export can break a caller elsewhere.
- **Convert test files after the code they test**, in a separate step, and don’t change assertions while converting. A test whose expected value changed in a “type-only” commit is a red flag.
- **Check the test runner handles `.ts`.** Vitest runs TypeScript files without extra setup; Jest needs a transform such as `ts-jest` or `babel-jest` with the TypeScript preset.
- **Add tests where types don’t reach.** Code that parses input, talks to an API or reads environment variables gets `any`-shaped data at runtime no matter what the types say. A few tests there are worth more than perfect annotations. [Write unit tests with AI](/en/blog/write-unit-tests-with-ai/ "Write unit tests with AI") shows how to get agents to write useful ones.

## Common pitfalls

- **Converting `require` to `import` changes semantics.** Default exports, `module.exports = fn` and interop differ between CommonJS and ES modules. Keep the module format change as its own step, or leave it out of the migration entirely.
- **Types that lie.** An interface written from one example object may miss fields that appear only sometimes. Prefer types written from real data or from the API schema.
- **Behaviour “fixes” hidden in a type commit.** `==` turned into `===`, a default parameter added, an `if` reordered. Each one can change behaviour. Reject them and ask for a separate commit.
- **Silencing the compiler with config.** `skipLibCheck` is fine; turning off a strict flag to make a branch pass is not. That’s why agents don’t get to edit `tsconfig.json`.
- **Generated or vendored files.** Exclude them in `tsconfig.json` rather than letting an agent “type” them.

## Doing it with AgentVera

AgentVera runs Claude Code, Codex, Gemini CLI and other CLI agents side by side per project, which suits a migration split into many small steps.

- **A worktree per folder.** From the [Git panel](/en/features/git-and-github/ "Git and GitHub") create a worktree for each directory and start an agent in it with one click, Claude Code on one folder and Codex on another if you like.
- **[File ownership](/en/features/file-ownership/ "File ownership").** Give the coordinating agent `tsconfig.json`, `package.json` and `src/types/**`. While it runs, other agents’ edits to those files are refused before they happen and they’re told to ask the owner instead; Claude Code agents are stopped by a hook, other CLIs get the rule in their instructions.
- **[Verify](/en/features/verify/ "Verify").** Write “typecheck passes, tests pass, no test assertion changed” in your message. When the turn ends, a separate verifier that can’t edit files runs the allowlisted test commands, reads the diff and marks each criterion pass or fail with evidence.
- **[Code review](/en/features/code-review/ "Code review").** A second model reads each finished turn and lists findings with file:line, which is a good place to catch new casts and `!` assertions.
- **[Merge queue](/en/features/merge-queue/ "Merge queue").** Folder branches land one at a time on the latest main, and your check command, such as `npm run typecheck && npm test`, runs on the merged result before anything is merged.
- **[Parallel team](/en/features/parallel-team/ "Parallel team") (Pro and Team).** An agent can split the migration into up to eight parts with the files each part may touch, run each part in its own worktree, merge the finished parts into one integration branch and run the final check, so you review one pull request.

## Wrapping up

Migrating JavaScript to TypeScript with AI works when the compiler and the tests are in charge, not the agent’s confidence. Start with a config that accepts JavaScript and sets `strict: false` on purpose, convert leaves first in small steps, keep `any` inside a budget that only shrinks, make `tsc` the agent’s exit check, ramp strictness flag by flag, and split parallel work by directory with shared files owned by one agent. Each step is boring and reviewable, which is exactly what you want from a migration.

If you’ll be watching agents step by step during the migration, [AI pair programming](/en/blog/ai-pair-programming/ "AI pair programming") explains when to let the agent drive and when to review every step.

## Frequently asked questions

**Can AI convert a JavaScript project to TypeScript?** Yes, and it is one of the jobs agents do well, as long as you give them a working type checker, a fixed order and small steps. Let the agent convert one file or folder at a time, run tsc and the tests after each step, and commit each step separately.

**Should I turn on strict mode from the start?** Not in an existing codebase. Since TypeScript 6.0 strict is on by default, so set "strict": false explicitly at first, convert the files, then turn on noImplicitAny, strictNullChecks and the other strict flags one at a time.

**How do I stop the agent from using any everywhere?** Make it measurable. Report explicit any with the typescript-eslint rule no-explicit-any, ban @ts-ignore with ban-ts-comment, keep a budget number in the repository and fail CI when the count goes above it. Tell the agent the budget in its instructions.

**Can several agents migrate different folders at the same time?** Yes, if each one works in its own git worktree and owns one directory. Shared files such as tsconfig.json, package.json and global type declarations should belong to a single agent, and each branch should be type-checked again after it is merged.

**Do I need to rewrite my tests during the migration?** No. Keep the existing tests running against the converted code first; they are what tells you the behaviour did not change. Convert test files after the module they cover, and avoid changing assertions in the same step.

---

**About AgentVera.** AgentVera is a desktop app for macOS, Windows and Linux, built by Solviera Teknoloji. It runs 18 AI coding agents (Claude Code, Codex, Gemini CLI, Copilot, Cursor, Antigravity, opencode, Hermes, Qwen Code, Kimi Code, Kiro, Amp, Droid, Goose, Vibe, Crush, Aider, Cline) side by side, per project, in real terminals with the user's own CLI installs and logins. The free plan has unlimited agents. Download: https://agentvera.dev/en/download/ · Pricing: https://agentvera.dev/en/pricing/
