مقدمة إلى استراتيجيات الترحيل
ترحيل قاعدة شفرة JavaScript موجودة إلى TypeScript لا يجب أن يكون عملية الكل أو لا شيء. يوفر TypeScript عدة ميزات تمكن من التبني التدريجي، مما يسمح لك بإضافة سلامة الأنواع تدريجياً إلى مشروعك مع الحفاظ على الوظائف الكاملة.
المرحلة 1: إعداد المشروع
ابدأ بتهيئة TypeScript في مشروعك:
# تثبيت TypeScript
npm install --save-dev typescript
# تهيئة tsconfig.json
npx tsc --init
# تثبيت تعريفات الأنواع لتبعياتك
npm install --save-dev @types/node @types/react @types/lodash
tsconfig.json الأولي للترحيل
قم بتكوين TypeScript للسماح بملفات JavaScript وتمكين الترحيل التدريجي:
{
"compilerOptions": {
// إعدادات أساسية للترحيل
"allowJs": true, // السماح بملفات JavaScript
"checkJs": false, // عدم فحص أنواع ملفات JS (بعد)
"noEmit": true, // عدم الإصدار أثناء الترحيل
"esModuleInterop": true,
"skipLibCheck": true,
// إعدادات الهدف والوحدة
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
// الترحيل التدريجي
"strict": false, // ابدأ بالتساهل، فعل لاحقاً
"noImplicitAny": false, // السماح بـ any ضمني أثناء الترحيل
// خرائط المصدر للتصحيح
"sourceMap": true,
// دليل الإخراج (عندما تبدأ الإصدار)
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
ملاحظة: ابدأ بـ "strict": false لتجنب الإرهاق بالأخطاء. قم بتمكين الوضع الصارم تدريجياً أثناء تحويل الملفات.
المرحلة 2: إعادة تسمية الملفات تدريجياً
ابدأ بتحويل الملفات من .js إلى .ts (أو .jsx إلى .tsx):
الاستراتيجية 1: من الأسفل إلى الأعلى (موصى بها)
ابدأ بملفات الأدوات والدوال النقية التي ليس لها تبعيات:
// قبل: utils.js
export function formatDate(date) {
return date.toISOString().split('T')[0];
}
export function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// بعد: utils.ts
export function formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
interface Item {
price: number;
}
export function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
الاستراتيجية 2: من الأعلى إلى الأسفل
ابدأ بنقاط الدخول واعمل أسفل شجرة التبعية:
// 1. تحويل main.js → main.ts
// 2. إصلاح الأخطاء في الاستيرادات
// 3. تحويل الملفات المستوردة واحداً تلو الآخر
// 4. كرر حتى يتم تحويل جميع الملفات
التعامل مع أنماط الترحيل الشائعة
1. تحويل الكائنات الفضفاضة إلى واجهات:
// قبل: JavaScript
const user = {
id: 1,
name: 'John Doe',
email: 'john@example.com'
};
function updateUser(user, updates) {
return { ...user, ...updates };
}
// بعد: TypeScript
interface User {
id: number;
name: string;
email: string;
}
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}
2. تحويل الاستدعاءات إلى دوال مكتوبة:
// قبل: JavaScript
function fetchData(url, onSuccess, onError) {
fetch(url)
.then(response => response.json())
.then(onSuccess)
.catch(onError);
}
// بعد: TypeScript
type SuccessCallback<T> = (data: T) => void;
type ErrorCallback = (error: Error) => void;
function fetchData<T>(
url: string,
onSuccess: SuccessCallback<T>,
onError: ErrorCallback
): void {
fetch(url)
.then(response => response.json())
.then(onSuccess)
.catch(onError);
}
3. تحويل الفئات:
// قبل: JavaScript
class UserService {
constructor(apiUrl) {
this.apiUrl = apiUrl;
this.cache = new Map();
}
async getUser(id) {
if (this.cache.has(id)) {
return this.cache.get(id);
}
const response = await fetch(`${this.apiUrl}/users/${id}`);
const user = await response.json();
this.cache.set(id, user);
return user;
}
}
// بعد: TypeScript
interface User {
id: number;
name: string;
email: string;
}
class UserService {
private cache: Map<number, User> = new Map();
constructor(private apiUrl: string) {}
async getUser(id: number): Promise<User> {
const cached = this.cache.get(id);
if (cached) {
return cached;
}
const response = await fetch(`${this.apiUrl}/users/${id}`);
const user = await response.json() as User;
this.cache.set(id, user);
return user;
}
}
استخدام @ts-check لفحص الأنواع التدريجي
أضف فحص الأنواع إلى ملفات JavaScript دون تحويلها:
// @ts-check
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}
add(1, 2); // ✓ صحيح
add(1, '2'); // ✗ خطأ: Argument of type 'string' is not assignable to parameter
استخدم JSDoc للأنواع المعقدة:
// @ts-check
/**
* @typedef {Object} User
* @property {number} id
* @property {string} name
* @property {string} email
*/
/**
* @param {User[]} users
* @returns {User[]}
*/
function sortUsersByName(users) {
return users.sort((a, b) => a.name.localeCompare(b.name));
}
إنشاء ملفات تعريف الأنواع
لملفات JavaScript التي لا يمكنك تحويلها بعد، أنشئ ملفات إعلان .d.ts:
// legacy-api.js (لا يمكن التحويل بعد)
export function legacyFunction(data) {
// تنفيذ معقد
return processData(data);
}
// legacy-api.d.ts (تعريفات الأنواع)
export interface LegacyData {
id: number;
value: string;
}
export function legacyFunction(data: LegacyData): string;
التعامل مع مكتبات الطرف الثالث
قم بتثبيت تعريفات الأنواع للمكتبات بدون أنواع مدمجة:
# البحث عن تعريفات الأنواع
npm search @types/library-name
# تثبيت تعريفات الأنواع
npm install --save-dev @types/library-name
# إذا لم توجد أنواع، أنشئ أنواعك الخاصة
# إنشاء: src/types/library-name.d.ts
declare module 'library-name' {
export function doSomething(param: string): number;
}
التعامل مع أنواع 'any'
استخدم any بشكل استراتيجي أثناء الترحيل، ثم استبدل تدريجياً:
// المرحلة 1: ترحيل سريع مع any
function processData(data: any): any {
return data.value * 2;
}
// المرحلة 2: إضافة أنواع محددة
interface InputData {
value: number;
}
function processData(data: InputData): number {
return data.value * 2;
}
// المرحلة 3: إضافة التحقق
function processData(data: InputData): number {
if (typeof data.value !== 'number') {
throw new Error('Invalid data');
}
return data.value * 2;
}
أفضل ممارسة: استخدم ESLint مع قاعدة @typescript-eslint/no-explicit-any لتتبع والقضاء تدريجياً على أنواع any أثناء تحويل الملفات.
تمكين الوضع الصارم تدريجياً
قم بتمكين الفحوصات الصارمة واحدة تلو الأخرى:
{
"compilerOptions": {
// الخطوة 1: ابدأ هنا
"strict": false,
"noImplicitAny": false,
// الخطوة 2: قم بالتمكين بعد تحويل ملفات الأدوات
"noImplicitAny": true,
// الخطوة 3: قم بالتمكين بعد تنظيف فحوصات null
"strictNullChecks": true,
// الخطوة 4: قم بالتمكين للفئات
"strictPropertyInitialization": true,
// الخطوة 5: قم بالتمكين لأنواع الدوال
"strictFunctionTypes": true,
// الخطوة 6: قم بتمكين علامات صارمة متبقية
"strictBindCallApply": true,
"noImplicitThis": true,
"alwaysStrict": true,
// الخطوة الأخيرة: قم بتمكين الوضع الصارم الكامل
"strict": true
}
}
مطبات الترحيل الشائعة والحلول
المطب 1: الإفراط في استخدام تأكيدات الأنواع
// ❌ سيء: تأكيدات كثيرة جداً
const data = JSON.parse(jsonString) as User;
const users = response.data as User[];
// ✓ أفضل: التحقق في وقت التشغيل
function parseUser(data: unknown): User {
if (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'name' in data
) {
return data as User;
}
throw new Error('Invalid user data');
}
const user = parseUser(JSON.parse(jsonString));
المطب 2: تجاهل الأخطاء مع @ts-ignore
// ❌ سيء: قمع الأخطاء
// @ts-ignore
const value = obj.unknownProperty;
// ✓ أفضل: إصلاح النوع
interface MyObject {
unknownProperty?: string;
}
const obj: MyObject = {};
const value = obj.unknownProperty;
المطب 3: عدم استخدام Unknown للبيانات الخارجية
// ❌ سيء: افتراض شكل بيانات API
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json(); // يرجع any!
}
// ✓ أفضل: التحقق من البيانات الخارجية
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data: unknown = await response.json();
if (isUser(data)) {
return data;
}
throw new Error('Invalid user data from API');
}
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
typeof (data as User).id === 'number' &&
'name' in data &&
typeof (data as User).name === 'string'
);
}
قائمة التحقق من الترحيل
عملية الترحيل خطوة بخطوة:
- تثبيت TypeScript وتهيئة tsconfig.json
- تعيين
allowJs: true، checkJs: false، strict: false
- تثبيت @types لجميع التبعيات
- إعادة تسمية ملفات الأدوات/المساعد أولاً (.js → .ts)
- إضافة أنواع إلى توقيعات الدوال
- إنشاء واجهات لهياكل البيانات
- تحويل المكونات/الوحدات واحدة تلو الأخرى
- تمكين
noImplicitAny: true
- تمكين
strictNullChecks: true
- تمكين العلامات الصارمة المتبقية
- إزالة جميع أنواع
any
- تمكين
strict: true
- تشغيل فحص كامل للأنواع:
tsc --noEmit
الأدوات للمساعدة في الترحيل
# ESLint مع TypeScript
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
# أداة تغطية الأنواع
npm install --save-dev type-coverage
# فحص تغطية الأنواع
npx type-coverage --detail
# أدوات الترحيل الآلي
npm install --save-dev ts-migrate
npx ts-migrate-full <project-directory>
مثال ترحيل من العالم الحقيقي
// قبل: user-service.js
class UserService {
constructor(config) {
this.apiUrl = config.apiUrl;
this.timeout = config.timeout || 5000;
}
async getUsers(filters) {
const params = new URLSearchParams(filters);
const response = await fetch(
`${this.apiUrl}/users?${params}`,
{ timeout: this.timeout }
);
return response.json();
}
async updateUser(id, updates) {
const response = await fetch(
`${this.apiUrl}/users/${id}`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
}
);
return response.json();
}
}
// بعد: user-service.ts
interface Config {
apiUrl: string;
timeout?: number;
}
interface User {
id: number;
name: string;
email: string;
}
interface UserFilters {
name?: string;
email?: string;
status?: string;
}
type UserUpdates = Partial<Omit<User, 'id'>>;
class UserService {
private readonly apiUrl: string;
private readonly timeout: number;
constructor(config: Config) {
this.apiUrl = config.apiUrl;
this.timeout = config.timeout ?? 5000;
}
async getUsers(filters?: UserFilters): Promise<User[]> {
const params = new URLSearchParams(filters as Record<string, string>);
const response = await fetch(
`${this.apiUrl}/users?${params}`,
{ signal: AbortSignal.timeout(this.timeout) }
);
if (!response.ok) {
throw new Error(`Failed to fetch users: ${response.statusText}`);
}
const data: unknown = await response.json();
if (Array.isArray(data)) {
return data as User[];
}
throw new Error('Invalid response format');
}
async updateUser(id: number, updates: UserUpdates): Promise<User> {
const response = await fetch(
`${this.apiUrl}/users/${id}`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
signal: AbortSignal.timeout(this.timeout)
}
);
if (!response.ok) {
throw new Error(`Failed to update user: ${response.statusText}`);
}
return response.json() as Promise<User>;
}
}
export { UserService, type Config, type User, type UserFilters, type UserUpdates };
قياس تقدم الترحيل
# عد ملفات TypeScript مقابل JavaScript
echo "TypeScript files:" && find src -name "*.ts" -o -name "*.tsx" | wc -l
echo "JavaScript files:" && find src -name "*.js" -o -name "*.jsx" | wc -l
# تغطية الأنواع
npx type-coverage --detail
# مثال على الإخراج:
# 2345 / 3000 87.50%
# type-coverage success: >= 80.00%
تحذير: لا تتعجل في الترحيل. من الأفضل أن يكون لديك 50٪ من شفرتك مكتوبة بشكل صحيح من 100٪ مليئة بأنواع any. خذ الوقت لفهم الأنواع وإضافة التحقق المناسب.
تمرين:
- خذ مشروع JavaScript صغير (أو أنشئ واحداً) مع 10 ملفات على الأقل
- قم بتهيئة TypeScript بإعدادات صديقة للترحيل
- قم بتحويل ملفات الأدوات أولاً، مضيفاً الأنواع المناسبة
- أنشئ واجهات لجميع هياكل البيانات
- قم بتمكين
noImplicitAny وإصلاح جميع الأخطاء
- قم بتمكين
strictNullChecks والتعامل مع null/undefined بشكل صحيح
- قس تغطية الأنواع قبل وبعد
- وثق الدروس المستفادة والأنماط الشائعة التي واجهتها