TypeScript: ترحيل مشروع TypeScript JS إلى TS
آخر تحديث: 2026-08-26
يُعد ترحيل مشروع جافا سكريبت قائم إلى TypeScript السيناريو الأكثر شيوعًا وعمليًّا — يشرح هذا الدرس كيفية إتمام عملية الترحيل بأمان وبشكل تدريجي.
1. نظرة عامة على استراتيجيات الهجرة
(1) ثلاث استراتيجيات للهجرة
| الإستراتيجية | السرعة | المخاطرة | السيناريوهات المناسبة |
|---|---|---|---|
| ترحيل كامل لمرة واحدة | سريع | عالي | مشاريع صغيرة (< 20 ملفًا) |
| الترحيل التدريجي ملفًا تلو الآخر | متوسط | منخفض | المشاريع المتوسطة إلى الكبيرة (موصى به) |
| الترحيل التدريجي لـ JSDoc | بطيء | منخفض جدًا | المشاريع الكبيرة/الحاسمة |
(2) نظرة عامة على خطوات الترحيل
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. الخطوة 1: تهيئة الإعدادات
(1) إنشاء ملف tsconfig.json
tsc --init
(2) التكوين الأولي الأساسي — الوضع المريح
{
"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"]
}
strict أو noImplicitAny في بداية عملية الترحيل — تأكد أولاً من أن المشروع يتم ترجمته بنجاح، ثم قم بتشديد القيود تدريجيًّا.
(3) تحديد نوع التركيب
# Install the type declarations required by the project
npm install @types/node @types/express @types/lodash --save-dev
3. الخطوة 2: استخدم allowJs لتمكين التوافق بين JS و TS
(1) تمكين allowJs
{
"compilerOptions": {
"allowJs": true
}
}
تسمح ميزة allowJs لمُجمِّع TypeScript بقبول ملفات .js — وبذلك يمكن أن تحتوي المشاريع على ملفات JS وTS معًا دون أن يؤثر كل منهما على الآخر.
(2) التوافق مع ملفات الاستيراد
// main.ts —— Can be imported .js Documents
import { helper } from "./utils"; // utils.js Just being there is enough
// utils.js —— JS Documents,Untyped Annotations
function helper(value) {
return value.toString();
}
(3) checkJs — التحقق الاختياري من أنواع جافا سكريبت
{
"compilerOptions": {
"checkJs": true
}
}
عند تمكين checkJs، سيقوم TypeScript أيضًا بالتحقق من وجود أخطاء في الأنواع في ملفات .js (استنادًا إلى تعليقات JSDoc واستنتاج الأنواع). يُنصح بإبقائه معطلاً خلال مرحلة الترحيل الأولية — ولا تقم بتمكينه إلا بعد تحويل ملفات JS تدريجيًّا إلى TS.
4. الخطوة 3: الترحيل ملفًا تلو الآخر
(1) تسلسل الهجرة
ابدأ بالملف الذي يحتوي على أقل عدد من التبعيات — أي الملف «الورقي» (الذي يحتوي على دوال مساعدة وثوابت وما إلى ذلك، دون استيراد ملفات من مشاريع أخرى):
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) خطوات ترحيل ملف واحد
// ── Before the Migration:utils.js ──
function formatPrice(price, currency) {
return currency + price.toFixed(2);
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
// ── After the migration:utils.ts ──
function formatPrice(price: number, currency: string = "¥"): string {
return currency + price.toFixed(2);
}
function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
(3) تقنيات الترحيل — ابدأ بـ any ثم قم بالتحسين
// 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;
}
▶ مثال: ترحيل ملف مسار Express
// ── Before the Migration:users.js ──
const express = require("express");
const router = express.Router();
router.get("/", async (req, res) => {
const users = await User.findAll();
res.json(users);
});
router.post("/", async (req, res) => {
const { name, email } = req.body;
const user = await User.create({ name, email });
res.status(201).json(user);
});
module.exports = router;
الناتج:
// API endpoint response (status 200)
// Returns JSON data
// ── After the migration:users.ts ──
import { Router, Request, Response } from "express";
const router = Router();
interface CreateUserBody {
name: string;
email: string;
}
router.get("/", async (_req: Request, res: Response) => {
const users = await User.findAll();
res.json(users);
});
router.post("/", async (req: Request<{}, {}, CreateUserBody>, res: Response) => {
const { name, email } = req.body;
const user = await User.create({ name, email });
res.status(201).json(user);
});
export default router;
5. تعليقات الأنواع في JSDoc
إذا كنت لا ترغب في تغيير امتداد الملف، فيمكنك استخدام JSDoc لإضافة نوع إلى ملف JS الخاص بك:
(1) التعليقات التوضيحية للأنواع الأساسية
// 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) الإشارة إلى أنواع JSDoc في ملفات TS
// main.ts
import { findUser } from "./utils"; // utils.js has JSDoc types
let user = await findUser(1); // ✅ Type inference is correct(From JSDoc)
(3) علامات أنواع JSDoc الشائعة
| العلامة | الغرض | مثال |
|---|---|---|
@type |
نوع المتغير | @type {string} |
@param |
نوع المعلمة | @param {number} x |
@returns |
نوع الإرجاع | @returns {string} |
@typedef |
تعريف النوع | @typedef {Object} User |
@property |
خصائص الكائن | @property {string} name |
@template |
المعلمات العامة | @template T |
@callback |
نوع الاستدعاء المرتد | @callback Handler |
6. العقبات الشائعة في عملية الترحيل
(1) المأزق الأول: الانفجار الضمني any
// 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
}
الحل: أولاً، احرص على تجميع المشروع بنجاح، ثم أضف تعليقات الأنواع واحدة تلو الأخرى، وأخيرًا قم بتعطيل noImplicitAny.
(2) المأزق الثاني: التضارب بين module.exports وimport
// JS For use with documents module.exports
module.exports = function() { /* ... */ };
// TS Import Requirements esModuleInterop
import fn from "./legacy"; // Required esModuleInterop: true
(3) المأزق الثالث: المكتبات الخارجية غير المُصنَّفة
import untypedLib from "untyped-lib"; // ❌ Cannot find the declaration file
// Temporary solution——Create shim.d.ts
declare module "untyped-lib" {
const lib: any;
export default lib;
}
(4) المأزق الرابع: فقدان النوع this
// 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
}
};
▶ مثال: إضافة أنواع إلى كائنات JavaScript البسيطة
// ── 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);
}
الناتج:
// Executed successfully
// ── 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);
}
▶ مثال: الترحيل من JSDoc إلى TypeScript
// ── 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);
}
الناتج:
// Executed successfully
// ── 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);
}
❓ أسئلة شائعة
tsc --noEmit إلى التكامل المستمر (CI) من أجل فحص الأنواع (دون إخراج ملفات). خلال المراحل المبكرة من عملية الترحيل، يُسمح باستخدام --noImplicitAny false لضمان عدم فشل التكامل المستمر (CI) بسبب مشكلات متعلقة بالأنواع. ومع تشديد المعايير تدريجيًّا، يصبح فحص الأنواع في التكامل المستمر (CI) أكثر صرامةً.ts-ignore أم ts-expect-error؟@ts-expect-error أولاً — فسيُظهر خطأً إذا لم يكن هناك خطأ في النوع في السطر التالي (لمنع نسيان إزالة التعليق بعد الإصلاح). @ts-ignore يتجاهل الأخطاء دون قيد أو شرط، مما قد يخفي الأخطاء التي تم إصلاحها بالفعل. كلاهما حلان مؤقتان؛ وفي النهاية، يجب عليك إصلاح مشكلات الأنواع.📖 ملخص
- استراتيجية الترحيل: ترحيل تدريجي، ملفًا تلو الآخر (موصى به)، يبدأ بالملفات الطرفية وينتهي بالملفات الأولية
- التكوين الأولي في الوضع المتساهل: تم تمكين خيار allowJs للسماح بالتعايش، وتم تعطيل خيار noImplicitAny، وتم تعطيل خيار strict
- الترحيل في خطوة واحدة: .js → .ts — أضف أولاً تعليقات الأنواع (باستخدام
anyكخيار بديل)، ثم قم بتحسين الأنواع - تعتبر تعليقات الأنواع في JSDoc حلاً وسطًا لا يغير امتدادات الملفات — وهو أمر مثالي للانتقال التدريجي في المشاريع الكبيرة
- الأخطاء الشائعة: «انفجار»
anyالضمني، وتعارضاتmodule.exports، ومكتبات الجهات الخارجية غير المحددة الأنواع، والأنواعthisالمفقودة - قم بتمكين الخيارات الصارمة تدريجيًا — فمع كل خيار يتم تمكينه، يتم إصلاح جميع الأخطاء
📝 تمارين
- تمرين أساسي (مستوى الصعوبة ⭐): قم بترحيل ملف أداة بسيط مكتوب بلغة جافا سكريبت (يحتوي على 3–5 دوال) إلى لغة تايب سكريبت — أضف تعليقات الأنواع للمعلمات والقيم المرجعة لكل دالة، وتأكد من نجاح عملية الترجمة.
- تمرين متقدم (مستوى الصعوبة: ⭐⭐): قم بتكوين
tsconfig.jsonلدعم المشاريع المختلطة بين JS و TS — قم بتمكينallowJs، واستخدم JSDoc لإضافة أنواع إلى ملف JS، ثم قم باستيراده واستخدامه في ملف TS. - التحدي (الصعوبة: ⭐⭐⭐): قم بمحاكاة عملية ترحيل مشروع Express — قم بتكوين
tsconfig، وأضف الأنواع إلى طلبات واستجابات Express، وتعامل مع أمان الأنواع لـreq.body(حدد واجهةbody)، وتعامل مع الأنواع لـreq.params.