TypeScript: Migrating a TypeScript JS Project to TS

Last updated: 2026-08-26

Migrating an existing JavaScript project to TypeScript is the most common and practical scenario—this lesson explains how to complete the migration safely and incrementally.

1. Overview of Migration Strategies

(1) Three Migration Strategies

Strategy Speed Risk Suitable Scenarios
One-time full migration Fast High Small projects (< 20 files)
Incremental File-by-File Migration Medium Low Medium-to-Large Projects (Recommended)
JSDoc Gradual Migration Slow Very Low Large/Critical Projects
📌 Recommendation: Incremental, file-by-file migration. First, get the TS compiler to accept the JS files, then change the file extensions from .js to .ts one by one, making sure the compilation passes after each file is changed.

(2) Overview of Migration Steps

TEXT 📖 Display only
1. Initialization tsconfig.json(Relaxed Mode)
2. Install @types packages
3. Open allowJs——TS and JS Coexistence
4. File by file .js → .ts(Starting with the leaf file)
5. Gradually Enable Strict Options
6. Finally Unlocked strict


2. Step 1: Initialize the Configuration

(1) Generate tsconfig.json

BASH
tsc --init

(2) Key Initial Configuration—Relaxed Mode

JSON
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "allowJs": true,
    "checkJs": false,
    "noImplicitAny": false,
    "strict": false,
    "outDir": "./dist",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
💡 Key Point: Do not enable strict or noImplicitAny at the beginning of the migration—first make sure the project compiles, then gradually tighten the restrictions.

(3) Declaration of Installation Type

BASH
# Install the type declarations required by the project
npm install @types/node --save-dev


3. Step 2: use allowJs to enable coexistence of JS and TS

(1) Enable allowJs

JSON
{
  "compilerOptions": {
    "allowJs": true
  }
}

allowJs allows the TypeScript compiler to accept .js files—projects can contain both JS and TS files without affecting each other.

(2) Import Compatibility

TYPESCRIPT
// main.ts —— Can be imported .js Documents
import { helper } from "./utils";  // utils.js Just being there is enough

// utils.js —— JS Documents,Untyped Annotations
export function helper(value) {
  return value.toString();
}

(3) checkJs—Optional JavaScript type checking

JSON
{
  "compilerOptions": {
    "checkJs": true
  }
}

When checkJs is enabled, TypeScript will also check for type errors in .js files (based on JSDoc comments and type inference). It is recommended to keep it disabled during the initial migration phase—enable it only after the JS files have been gradually converted to TS.



4. Step 3: Migrate File by File

(1) Migration Sequence

Start with the file that has the fewest dependencies—the "leaf" file (which contains utility functions, constants, etc., without importing files from other projects):

TEXT 📖 Display only
Recommended Migration Order:
1. Constants File(config.js → config.ts)
2. Utility Functions(utils.js → utils.ts)
3. Type Definitions(types.js → types.ts)
4. Data Model(models.js → models.ts)
5. Service Layer(services.js → services.ts)
6. Controller/Routing(controllers.js → controllers.ts)
7. Input File(index.js → index.ts)

(2) Steps for Migrating a Single File

TYPESCRIPT
// ── Before the Migration:utils.js ──
export function formatPrice(price, currency) {
  return currency + price.toFixed(2);
}

export function clamp(value, min, max) {
  return Math.min(Math.max(value, min), max);
}
TYPESCRIPT
// ── After the migration:utils.ts ──
export function formatPrice(price: number, currency: string = "¥"): string {
  return currency + price.toFixed(2);
}

export function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(value, min), max);
}

(3) Migration Techniques—Start with any and then Refine

TYPESCRIPT
// Step 1:Add a type annotation,Uncertain usage any
function process(data: any, options: any): any {
  return data.filter(item => item.active);
}

// Step 2:Gradual Replacement any For a specific type
interface DataItem {
  id: number;
  name: string;
  active: boolean;
}

interface Options {
  limit?: number;
  sort?: "asc" | "desc";
}

function process(data: DataItem[], options: Options = {}): DataItem[] {
  let result = data.filter(item => item.active);
  if (options.sort === "desc") result.reverse();
  if (options.limit) result = result.slice(0, options.limit);
  return result;
}

▶ Example: Migrating a Node.js route file

TYPESCRIPT
// ── Before the Migration:users.js ──
const http = require("http");

const server = http.createServer(async (req, res) => {
  if (req.url === "/users" && req.method === "GET") {
    const users = await findAllUsers();
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(users));
  }
});

server.listen(3000);
▶ Try it Yourself
TYPESCRIPT
// ── After the migration:users.ts ──
import http from "http";

interface User {
  id: number;
  name: string;
  email: string;
}

async function findAllUsers(): Promise<User[]> {
  return [{ id: 1, name: "Charlie", email: "c@example.com" }];
}

const server = http.createServer(async (req, res) => {
  if (req.url === "/users" && req.method === "GET") {
    const users: User[] = await findAllUsers();
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(users));
  }
});

server.listen(3000);


5. JSDoc Type Annotations

If you don't want to change the file extension, you can use JSDoc to add a type to your JS file:

(1) Basic Type Annotations

JAVASCRIPT
// utils.js — Use JSDoc to add types
/**
 * Pricing Format
 * @param {number} price - Price
 * @param {string} [currency="¥"] - Currency Symbol
 * @returns {string} Formatted Price
 */
function formatPrice(price, currency = "¥") {
  return currency + price.toFixed(2);
}

/**
 * @typedef {Object} User
 * @property {number} id
 * @property {string} name
 * @property {string} email
 */

/**
 * Search for a User
 * @param {number} id
 * @returns {Promise<User>}
 */
async function findUser(id) {
  // ...
}

module.exports = { formatPrice, findUser };

(2) Referencing JSDoc types in TS files

TYPESCRIPT
// main.ts
import { findUser } from "./utils";  // utils.js has JSDoc types

let user = await findUser(1);  // ✅ Type inference is correct(From JSDoc)

(3) Common JSDoc Type Tags

Tag Purpose Example
@type Variable Type @type {string}
@param Parameter Type @param {number} x
@returns Return Type @returns {string}
@typedef Define Type @typedef {Object} User
@property Object Properties @property {string} name
@template Generic Parameters @template T
@callback Callback Type @callback Handler


6. Common Migration Pitfalls

(1) Pitfall 1: Implicit any Explosion

TYPESCRIPT
// A large number of implicit variables after migration any——Don't rush to start it noImplicitAny
function process(data) {  // data Implicit any
  return data.map(item => item.name);  // item Me too any
}

Solution: First, get the project to compile successfully, then add type annotations one by one, and finally disable noImplicitAny.

(2) Pitfall 2: Conflict between module.exports and import

TYPESCRIPT
// JS For use with documents module.exports
module.exports = function() { /* ... */ };

// TS Import Requirements esModuleInterop
import fn from "./legacy";  // Required esModuleInterop: true

(3) Pitfall 3: Untyped Third-Party Libraries

TYPESCRIPT
import legacyModule from "legacy-module";  // ❌ Cannot find the declaration file

// Temporary solution——Create shim.d.ts
declare module "legacy-module" {
  const lib: any;
  export default lib;
}

(4) Pitfall 4: Loss of the this type

TYPESCRIPT
// JS in this Dynamic Binding
const obj = {
  name: "Charlie",
  greet() {
    console.log(this.name);  // ✅ JS in OK
  }
};

// TS in this Needs annotation
const obj2 = {
  name: "Charlie",
  greet(this: { name: string }) {
    console.log(this.name);  // ✅ TS Needed in this Parameters
  }
};

▶ Example: Adding Types to Plain JavaScript Objects

TYPESCRIPT
// ── Before: shapes.js ──
const shapes = [
  { type: "circle", radius: 5 },
  { type: "rectangle", width: 10, height: 20 },
  { type: "circle", radius: 3 }
];

function totalArea(shapes) {
  return shapes.reduce((sum, s) => {
    if (s.type === "circle") return sum + Math.PI * s.radius * s.radius;
    return sum + s.width * s.height;
  }, 0);
}
▶ Try it Yourself
TYPESCRIPT
// ── After: shapes.ts ──
interface Circle { type: "circle"; radius: number; }
interface Rectangle { type: "rectangle"; width: number; height: number; }
type Shape = Circle | Rectangle;

const shapes: Shape[] = [
  { type: "circle", radius: 5 },
  { type: "rectangle", width: 10, height: 20 },
  { type: "circle", radius: 3 }
];

function totalArea(shapes: Shape[]): number {
  return shapes.reduce((sum, s) => {
    if (s.type === "circle") return sum + Math.PI * s.radius * s.radius;
    return sum + s.width * s.height;
  }, 0);
}

▶ Example: JSDoc to TypeScript Migration

JAVASCRIPT
// ── Before: math.js (JSDoc-typed) ──
/**
 * @template T
 * @param {T[]} arr
 * @param {(item: T) => boolean} predicate
 * @returns {T[]}
 */
function filter(arr, predicate) {
  return arr.filter(predicate);
}

/**
 * @typedef {Object} Point
 * @property {number} x
 * @property {number} y
 */

/**
 * @param {Point} a
 * @param {Point} b
 * @returns {number}
 */
function distance(a, b) {
  return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}
▶ Try it Yourself
TYPESCRIPT
// ── After: math.ts (native TS types) ──
function filter<T>(arr: T[], predicate: (item: T) => boolean): T[] {
  return arr.filter(predicate);
}

interface Point {
  x: number;
  y: number;
}

function distance(a: Point, b: Point): number {
  return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}

❓ FAQ

Q How long does it take to migrate a large project?
A It depends on the amount of code and the team. For small to medium-sized projects (10,000 rows or fewer), it takes about 1–2 weeks. Large projects (100,000 rows or more) may require 1–3 months of incremental migration. The key is not to make all the changes at once—migrate one file at a time, ensuring that the code compiles successfully after each step.
Q Which is better, JSDoc type annotations or TypeScript type annotations?
A TypeScript type annotations are better—they offer more concise syntax, are more powerful, and have better editor support. JSDoc is a compromise that doesn’t require changing the file extension, making it suitable for situations where you can’t modify the filename. The ultimate goal is still to convert JS files to TypeScript.
Q What about CI during the migration?
A Add tsc --noEmit to CI for type checking (without outputting files). During the early stages of migration, --noImplicitAny false is allowed to ensure that CI does not fail due to type issues. As the process gradually tightens, type checking in CI becomes increasingly strict.
Q Which should I use, ts-ignore or ts-expect-error?
A Use @ts-expect-error first—it will throw an error if there is no type error on the next line (to prevent forgetting to remove the comment after the fix). @ts-ignore ignores errors unconditionally, which may mask errors that have already been fixed. Both are temporary workarounds; ultimately, you should fix the type issues.

📖 Summary

📝 Exercises

  1. Basic Exercise (Difficulty ⭐): Migrate a simple JavaScript utility file (3–5 functions) to TypeScript—add type annotations for parameters and return values to each function, and ensure it compiles successfully.
  2. Advanced Exercise (Difficulty: ⭐⭐): Configure tsconfig.json to support mixed JS/TS projects—enable allowJs, use JSDoc to add types to a JS file, then import and use it in a TS file.
  3. Challenge (Difficulty: ⭐⭐⭐): Simulate migrating a Node.js HTTP server project—configure tsconfig, add types to the IncomingMessage and ServerResponse objects, handle type safety for request body parsing (define the body interface), and handle URL parameter extraction.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏