Migrating a project from JavaScript to TypeScript is one of the more common decisions developers face as a codebase grows and the cost of runtime bugs increases. The good news is that TypeScript is designed for incremental adoption. There is no requirement to type every file before the project becomes useful. A partial migration still produces real benefits: better editor support, compiler-enforced null checking in the files that have been converted, and safer refactoring in the areas that matter most.
This guide covers a practical five-step migration path. Each step builds on the previous one and produces a working codebase at every stage, so the migration can be paused and resumed without leaving the project in a broken state. **What this covers:
Installing TypeScript and configuring
tsconfig.jsonRenaming files and enabling incremental compilation
Fixing initial type errors without blocking progress
Enabling stricter compiler checks gradually
Replacing
anywith specific types over time
Step 1: Install TypeScript and Generate a Config
Install TypeScript as a development dependency:
npm install -D typescriptGenerate a tsconfig.json with default settings:
npx tsc --initThe generated file contains a large number of commented-out options. The relevant settings for a migration are:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": false,
"allowJs": true,
"checkJs": false,
"outDir": "./dist",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}Setting strict: false and allowJs: true at the start allows JavaScript and TypeScript files to coexist in the same project without requiring every file to be fully typed immediately. checkJs: false prevents TypeScript from type-checking plain .js files, which would produce a large number of errors before any migration work has been done.
For React projects, add "jsx": "react-jsx" to compilerOptions. For Next.js or Vite, check the framework's documentation for the recommended tsconfig base configuration, as both provide starter configurations that account for framework-specific requirements.
Step 2: Rename Files from .js to .ts
Start with a small, self-contained part of the codebase: a utility module, a set of helper functions, or a single service file. Rename the .js files to .ts.
For files containing JSX (React components), rename to .tsx rather than .ts. TypeScript requires the .tsx extension to parse JSX syntax correctly.
Renaming does not immediately make the code typed. It tells the TypeScript compiler to process the file and report type errors. At this stage, the editor will begin showing type hints and flagging obvious issues. Expect to see errors — they are the migration pointing out what needs attention.
A practical migration order:
Utility functions and helpers (fewest dependencies, easiest to type)
Type definitions and interfaces (enables typing the rest of the codebase)
Service and data-access layers
Components or controllers
Entry points and configuration files
Converting files in this order means the foundational types are in place before the files that depend on them are converted.
Step 3: Fix Initial Type Errors
After renaming the first batch of files, address the errors TypeScript reports. The goal at this stage is to make the project compile without changing application behavior.
The most common initial errors:
Implicit any on function parameters. TypeScript infers any when a parameter has no type annotation, which it may flag depending on configuration. Add a specific type if it is straightforward; use any as a temporary placeholder if the correct type is not immediately clear:
// Temporary — replace with a specific type later
function greet(name: any): string {
return `Hello, ${name}`;
}Missing type definitions for external packages. If a library does not ship with TypeScript types, the compiler will report it cannot find a type declaration for the module. Install the corresponding @types/ package:
npm install -D @types/lodash
npm install -D @types/expressMost popular packages have community-maintained type definitions via the DefinitelyTyped repository. Search at npmjs.com for @types/package-name to check availability. If no types exist, a minimal declaration file suppresses the error:
// src/types/missing-module.d.ts
declare module 'package-without-types';Object property access errors. TypeScript may flag property access on objects whose shape is not yet defined. Defining an interface or type for the object resolves this:
interface User {
id: number;
name: string;
email: string;
}The measure of success at this stage is a clean npx tsc --noEmit run with no compilation errors. Application behavior should be identical to before the migration started.
Step 4: Enable Stricter Checks Gradually
Once the project compiles cleanly with the initial configuration, stricter compiler options can be enabled incrementally. Turning on strict: true immediately on a partially-migrated codebase will produce a large number of errors. Enabling flags one at a time is more manageable.
A recommended progression:
// Phase 1 — enable after initial migration
{
"compilerOptions": {
"noImplicitAny": true
}
}noImplicitAny requires every parameter and variable to have an explicit type or a type that can be inferred from context. This is the single highest-value strictness flag because it eliminates the implicit any placeholders that undermine type safety.
// Phase 2 — enable after noImplicitAny is clean
{
"compilerOptions": {
"strictNullChecks": true
}
}strictNullChecks treats null and undefined as distinct types rather than assignable to everything. This forces explicit handling of nullable values, which prevents the most common category of JavaScript runtime errors.
// Phase 3 — enable full strict mode
{
"compilerOptions": {
"strict": true
}
}strict: true enables noImplicitAny, strictNullChecks, and several additional checks together. Reaching this point means the codebase has the full benefits of TypeScript's type system.
Fix all errors introduced by each flag before enabling the next one. The incremental approach makes each batch of errors manageable and keeps the project in a compilable state throughout.
Step 5: Replace any with Specific Types
With strict mode enabled, the remaining work is replacing the any placeholders used during migration with accurate type annotations. This is where the long-term maintainability benefits of TypeScript are fully realized.
Specific replacements to look for:
Primitive types for straightforward values:
function greet(name: string): string {
return `Hello, ${name}`;
}Interfaces and type aliases for objects:
interface ApiResponse {
data: User[];
total: number;
page: number;
}unknown instead of any for values whose type is genuinely uncertain. Unlike any, unknown requires a type check before the value can be used:
function parseInput(input: unknown): string {
if (typeof input === 'string') {
return input.toUpperCase();
}
throw new TypeError('Expected a string');
}Generic types for reusable functions and data structures:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}This step has no fixed end date. In a large codebase, replacing all uses of any is a long-term effort. Prioritize the types that provide the most value: API boundaries, shared utility functions, and the data models that flow through the most code paths.
Migration Checklist
Step | Task | Success criteria |
|---|---|---|
1 | Install TypeScript, generate |
|
2 | Rename files to | Files are processed by the compiler |
3 | Fix initial errors, use | Project compiles cleanly with no errors |
4 | Enable | Each phase compiles cleanly |
5 | Replace |
|
Key Takeaways
Start with
strict: falseandallowJs: trueto allow JavaScript and TypeScript files to coexist during migration.Convert files in dependency order: utilities and type definitions first, components and entry points last.
Use
anyas a temporary placeholder during initial conversion. Its purpose is to keep the project compiling while migration is in progress, not as a permanent type.Enable strictness flags incrementally:
noImplicitAnyfirst, thenstrictNullChecks, then fullstrictmode.unknownis a safer alternative toanyfor values whose type is genuinely uncertain. It requires a type guard before use.Replacing
anywith accurate types is a long-term effort. Prioritize API boundaries and shared data models.
Conclusion
A JavaScript-to-TypeScript migration is not a single event. It is a gradual process that improves the codebase at each step. The approach described here keeps the project working throughout, produces meaningful type coverage early, and allows strictness to be increased at a pace that matches the team's capacity.
A partial migration is better than no migration. Even converting the core utilities and data models while leaving other files as JavaScript provides measurable benefits in the areas that matter most.
Working through a migration and hitting a specific problem — a tricky type, a library without definitions, or a configuration that isn't behaving as expected? Describe it in the comments.




