Tutorials & Guides
How to automate JS to TypeScript migration using Claude 3.5 Sonnet and LLM chaining
Migrating legacy JS to TypeScript is a recipe for a headache. This step-by-step tutorial shows you how to build a multi-step chain with Claude 3.5 Sonnet to safely automate the transition.
Updated 8/18/2026
The multi-file refactoring headache
We have all promised ourselves we would finally migrate that old Node.js utility or side project to TypeScript. And we have all given up after spending four hours manually writing interface definitions and fighting compiler errors.
Throwing a raw .js file at an LLM and asking it to "convert this to TypeScript" rarely works for anything larger than a simple utility function. The model gets lazy, defaults to using any for complex types, or forgets to import external dependencies entirely.
To do this properly, we need a reliable, step-by-step pipeline. In this tutorial, we will build a Python automation CLI that uses /platforms/claude to migrate legacy JavaScript files to strict, production-ready TypeScript. By chaining focused prompts together, we get robust migrations without the developer fatigue.
Why LLM chaining beats single-shot prompts
When migrating code, an LLM has to make multiple complex architectural decisions simultaneously. It needs to infer parameter types, generate interface definitions, map internal logic, and rewrite syntax.
If we split this into a multi-step chain, we drastically improve code quality: 1. Step 1 (Analysis): Read the target JavaScript file and run a fast analysis to detect parameter usage and return structures. 2. Step 2 (Type Generation): Generate a clean, typed declaration file or inline interfaces based on the analysis. 3. Step 3 (Conversion): Rewrite the code to TypeScript, strictly applying the new type definitions.
This orchestrated pipeline keeps our development workflow ticking along without manual intervention. For more details on designing robust pipelines like this, check out our guide on creating structured templates in our /prompts builder.
Step 1: Setting up the migration tool
We will use Python to coordinate our file reading, script execution, and API calls. First, make sure you have the Anthropic Python library installed:
`bash
pip install anthropic
`
Next, ensure your Anthropic API key is exported in your environment:
`bash
export ANTHROPIC_API_KEY="your_claude_api_key_here"
`
Step 2: Defining the pipeline code
Create a file named migrate.py. We will build a modular script that executes our migration chain sequentially.
`python
import os
import sys
from pathlib import Path
from anthropic import Anthropic
client = Anthropic()
def call_claude(prompt: str) -> str:
"""Utility function to query Claude 3.5 Sonnet with a clean system prompt."""
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=4000,
temperature=0.0, # Zero temperature is vital for consistent code translation
system="You are an expert principal software engineer specializing in strict TypeScript migrations. Output only clean code, avoiding chatty preambles.",
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
`
Setting temperature=0.0 ensures Claude behaves deterministically, reducing creative flights of fancy and keeping type inferences consistent.
Step 3: Chain Link 1 — Static Type Inference
Our first link in the chain reads the JavaScript code and generates a raw dictionary of types. It acts as an automated static analyser.
`python
def run_type_analysis(js_code: str) -> str:
prompt = f"""
Analyze the following JavaScript code and extract the implicit types for all functions, inputs, and objects.
Provide a detailed layout of the expected interfaces in TypeScript format.
JavaScript Code:
`javascript
{js_code}
`
Output only the proposed TypeScript interfaces, type definitions, or custom types. Do not write the full logic code yet.
"""
return call_claude(prompt)
`
By running this step separately, we isolate the "type-guessing" phase from the actual code rewrite phase. This prevents Claude from forgetting types mid-way through a long rewrite.
Step 4: Chain Link 2 — The Code Conversion
Now, we feed both the original JavaScript and the inferred type interfaces back to Claude, instructing it to output the final TypeScript file.
`python
def convert_to_typescript(js_code: str, types_definition: str) -> str:
prompt = f"""
You are converting a legacy JavaScript file to strict TypeScript.
Here is the original JavaScript:
`javascript
{js_code}
`
Here are the predefined TypeScript interfaces and types to enforce:
`typescript
{types_definition}
`
Rewrite the code entirely in strict TypeScript.
- Do not use 'any' types under any circumstances. If a type is unknown, use 'unknown'.
- Preserve all original comments, JSDoc annotations, and business logic.
- Ensure all required imports/exports match standard ESM (import/export) syntax.
Provide ONLY the raw, compile-ready TypeScript output inside standard markdown blocks.
"""
return call_claude(prompt)
`
Step 5: File processing and extraction
Finally, we need a CLI handler to read the input .js file, run the pipeline, strip away any markdown block wrappers Claude outputs, and save the result to a .ts file.
`python
def clean_code_blocks(text: str) -> str:
"""Helper to strip away markdown code block symbols if Claude outputs them."""
lines = text.splitlines()
clean_lines = []
for line in lines:
if line.strip().startswith("`"):
continue
clean_lines.append(line)
return "\n".join(clean_lines).strip()
def migrate_file(input_path: Path): if not input_path.exists(): print(f"Error: File {input_path} does not exist.") return print(f"Reading legacy code from {input_path.name}...") js_code = input_path.read_text(encoding='utf-8') print("[Step 1/2] Generating TypeScript Type Definitions...") types_def = run_type_analysis(js_code) print("[Step 2/2] Generating final TS code using definitions...") raw_ts = convert_to_typescript(js_code, types_def) clean_ts = clean_code_blocks(raw_ts) output_path = input_path.with_suffix(".ts") output_path.write_text(clean_ts, encoding='utf-8') print(f"Success! Ported file written to {output_path.name}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python migrate.py <path-to-js-file>")
sys.exit(1)
migrate_file(Path(sys.argv[1]))
`
Testing our pipeline
Let’s test our new tool with a classic piece of dynamic, type-unsafe JavaScript. Create a temporary file called userCache.js with the following dynamic behaviour:
`javascript
// Legacy userCache.js
const db = require('./fake-db');
function processUserRecord(user, options) { const prefix = options.verbose ? '[LOG] ' : ''; if (user.age && user.age > 18) { console.log(prefix + "Processing adult account: " + user.name); return db.save({ id: user.id, status: 'active', meta: user.meta }); } return null; }
module.exports = { processUserRecord };
`
Run the migration tool:
`bash
python migrate.py userCache.js
`
Within seconds, you will find a beautifully typed userCache.ts sitting next to your legacy file. The output will look something like this:
`typescript
import db from './fake-db';
interface UserMeta { [key: string]: unknown; }
interface User { id: string | number; name: string; age?: number; meta?: UserMeta; }
interface ProcessOptions { verbose: boolean; }
export function processUserRecord(user: User, options: ProcessOptions): unknown {
const prefix = options.verbose ? '[LOG] ' : '';
if (user.age && user.age > 18) {
console.log(prefix + "Processing adult account: " + user.name);
return db.save({ id: user.id, status: 'active', meta: user.meta });
}
return null;
}
`
Note how Claude intelligently created interfaces for User and ProcessOptions separately from the code translation step. If we had ran this in a single shot, it would have been highly likely to slip into an inline declaration like user: any to save space.
Troubleshooting pipeline issues
If you find Claude is generating type definitions that do not match up with real-world requirements, you can adjust the static type inference prompt to import helper types directly from your existing workspace types.
In case your pipeline encounters connection timeouts during large files, verify that your client limits match your API plan tier. For further detailed support on API limits and connection timeouts, you can check out the official Claude Support site.
Now, hook this script up to your terminal and start migrating those legacy folders cleanly, one step at a time.
Keep going
Build something with the prompt generator, decode the jargon in the glossary, or compare the tools on our platform deep-dives.