Guided Tutorials
Migrate JavaScript to TypeScript
Add type safety to an existing JavaScript project file by file with proper type definitions. This tutorial migrates a src/utils/ directory, but the same approach scales to an entire codebase.
What you'll learn
- How to plan a bottom-up migration order based on file dependencies
- How to configure TypeScript for incremental migration with mixed JS/TS files
- How to prompt Autohand to generate proper type annotations and interfaces
- How to review generated types and fix compiler errors iteratively
Before you start
- Autohand Code installed
- Node.js 18 or newer
- A JavaScript project with a
package.json - Git with a clean working tree for clean diffs
Plan the migration order
The safest migration order is bottom-up: leaf modules with no internal dependencies first, then the modules that depend on them, then the entry points last.
Ask Autohand to map the dependency graph for your target directory:
bash
autohand "Map the import dependencies between files in src/utils/. Show me which files have no imports from other local files so I know where to start the TypeScript migration."You will get output like:
text
Dependency order for src/utils/ (migrate in this order):
1. No local dependencies (start here):
src/utils/format.js
src/utils/constants.js
src/utils/errors.js
2. Depends on layer 1:
src/utils/validate.js (imports: format.js)
src/utils/http.js (imports: errors.js)
3. Depends on layer 2:
src/utils/auth.js (imports: validate.js, http.js)Work through this list in order. Each file you migrate reduces the unknown surface area for the next one.
Configure TypeScript
Before migrating any files, set up a TypeScript configuration that allows mixed JS/TS files. This means the project keeps building throughout the migration.
bash
autohand "Add TypeScript to this project. Install the required packages, create a tsconfig.json that allows incremental migration (allowJs: true, noEmit: true for now), and make sure the build still works after the config is added."The resulting tsconfig.json will look something like this:
json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"allowJs": true,
"checkJs": false,
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"paths": {}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}With allowJs: true, TypeScript is happy to import from both .js and .ts files. You can migrate one file at a time without breaking anything.
Run the migration prompt
Start with the first leaf module from your dependency order. The prompt instructs Autohand to rename the file, add annotations, and extract interfaces.
bash
autohand "Migrate src/utils/format.js to TypeScript. Rename it to format.ts. Add parameter and return types to every function. If any function accepts or returns an object, extract a named interface for it. Do not use 'any' as a type."For the second layer of files, reference the already-migrated types:
bash
autohand "Migrate src/utils/validate.js to TypeScript. It imports from format.ts which is already typed. Use the existing types from format.ts wherever they apply. Add types for the remaining parameters and return values."Tip: If Autohand generates a type you are not sure about, ask it to explain: autohand "Explain the ValidationResult interface you just created and why each field is typed the way it is."
Review generated types
Here is what a typical migration output looks like. The original JavaScript:
javascript
// src/utils/validate.js (before)
export function validateEmail(email) {
return /^[^s@]+@[^s@]+.[^s@]+$/.test(email.trim());
}
export function validateUser(user) {
const errors = {};
if (!user.email) errors.email = 'Email is required';
if (!user.name) errors.name = 'Name is required';
return { valid: Object.keys(errors).length === 0, errors };
}The migrated TypeScript:
typescript
// src/utils/validate.ts (after)\ninterface UserInput {\n email: string;\n name: string;\n [key: string]: unknown;\n}\n\ninterface ValidationResult {\n valid: boolean;\n errors: Partial<Record<keyof UserInput, string>>;\n}\n\nexport function validateEmail(email: string): boolean {\n return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());\n}\n\nexport function validateUser(user: UserInput): ValidationResult {\n const errors: Partial<Record<keyof UserInput, string>> = {};\n if (!user.email) errors.email = 'Email is required';\n if (!user.name) errors.name = 'Name is required';\n return { valid: Object.keys(errors).length === 0, errors };\n}The key things to check in each migrated file are:
- No
anytypes unless absolutely unavoidable (and if so, there should be a comment explaining why) - Interfaces are placed in the file where the shape is first defined, or in a shared
types.tsif used across multiple files - Optional fields are marked with
?, not typed asT | undefined - Function return types are explicit on public exports
Fix type errors
After migrating each file, run the TypeScript compiler to catch errors before moving forward.
bash
npx tsc --noEmitWhen type errors appear, paste them back to Autohand:
bash
autohand "Fix these TypeScript errors in src/utils/auth.ts:
src/utils/auth.ts:34:5 - error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
src/utils/auth.ts:67:12 - error TS7006: Parameter 'token' implicitly has an 'any' type."Autohand will read the file, understand the context around each error, and propose specific fixes. For the errors above, it might tighten the input validation upstream or add a type guard to narrow the union type.
Tip: If you see many TS7006 implicitly has any type errors, it means the function parameters were not typed. Rather than fixing them one by one, run the original migration prompt again with stricter instructions: autohand "Re-migrate this file and ensure every parameter has an explicit type annotation."
Migrate the next batch
Once the first directory is done and the compiler reports zero errors, move to the next area of the codebase. Update the migration prompt to include context from what you have already done:
bash
autohand "Migrate src/api/ to TypeScript. The types in src/utils/ are already defined. Import and reuse those types wherever applicable rather than redefining them. Start with the files that have no local dependencies."When the entire codebase is migrated, tighten the TypeScript config:
json
{
"compilerOptions": {
"allowJs": false,
"checkJs": false,
"strict": true,
"noUncheckedIndexedAccess": true
}
}Setting allowJs: false at the end ensures no new JavaScript files can slip into the codebase going forward. Run npx tsc --noEmit one final time and fix any remaining errors before merging.
What you learned
- You mapped file dependencies and migrated leaf modules first to avoid breaking imports
- You configured TypeScript for incremental migration with
allowJs: true - You prompted Autohand to add type annotations, extract interfaces, and avoid
any - You tightened the TypeScript config after completing the migration to prevent new JS files