لغة TypeScript

الوحدات والمساحات الاسمية

30 دقيقة الدرس 15 من 40

تنظيم الكود باستخدام الوحدات والمساحات الاسمية

مع نمو تطبيقات TypeScript، يصبح تنظيم الكود أمراً حاسماً. TypeScript يوفر آليتين أساسيتين لتنظيم الكود: وحدات ES (المعيار الحديث) والمساحات الاسمية (قديم ولكن لا يزال مفيداً). فهم كليهما ضروري للعمل مع قواعد كود TypeScript.

وحدات ES في TypeScript

وحدات ES هي الطريقة القياسية لتنظيم كود JavaScript/TypeScript. توفر طريقة نظيفة وصريحة لمشاركة الكود بين الملفات:

// math.ts - ملف وحدة export function add(a: number, b: number): number { return a + b; } export function subtract(a: number, b: number): number { return a - b; } export const PI = 3.14159; // يمكن أيضاً التصدير بعد التصريح function multiply(a: number, b: number): number { return a * b; } export { multiply }; // main.ts - استيراد الوحدة import { add, subtract, PI } from "./math"; console.log(add(5, 3)); // 8 console.log(subtract(10, 4)); // 6 console.log(PI); // 3.14159 // الاستيراد مع الاسم المستعار import { multiply as mult } from "./math"; console.log(mult(4, 5)); // 20 // استيراد جميع الصادرات كمساحة اسمية import * as Math from "./math"; console.log(Math.add(1, 2)); // 3 console.log(Math.PI); // 3.14159
ملاحظة: وحدات ES يتم تحليلها بشكل ثابت، مما يعني أن الاستيرادات والصادرات يتم حلها في وقت الترجمة. هذا يمكّن من tree-shaking (إزالة الكود غير المستخدم) ودعم أفضل لـ IDE مع IntelliSense.

الصادرات الافتراضية (Default Exports)

يمكن أن تحتوي الوحدات على صادر افتراضي واحد، يتم استيراده بدون أقواس معقوفة:

// user.ts - صادر افتراضي export default class User { constructor( public name: string, public email: string ) {} greet(): string { return `Hello, I'm ${this.name}`; } } // يمكن أيضاً استخدام عبارة export default منفصلة class Product { constructor(public name: string, public price: number) {} } export default Product; // main.ts - استيراد صادر افتراضي import User from "./user"; import Product from "./product"; const user = new User("Alice", "alice@example.com"); console.log(user.greet()); // "Hello, I'm Alice" const product = new Product("Laptop", 999); console.log(product.price); // 999 // يمكن تسمية الاستيراد الافتراضي بأي شيء import MyUser from "./user"; const user2 = new MyUser("Bob", "bob@example.com"); // دمج الصادرات الافتراضية والمسماة // utils.ts export default function log(message: string): void { console.log(`[LOG] ${message}`); } export function error(message: string): void { console.error(`[ERROR] ${message}`); } export function warn(message: string): void { console.warn(`[WARN] ${message}`); } // main.ts import log, { error, warn } from "./utils"; log("Application started"); error("Something went wrong"); warn("Deprecated feature used");

إعادة التصدير (Re-exporting)

يمكن للوحدات إعادة تصدير عناصر من وحدات أخرى، مفيد لإنشاء ملفات البراميل:

// models/user.ts export interface User { id: number; name: string; email: string; } // models/product.ts export interface Product { id: number; name: string; price: number; } // models/order.ts export interface Order { id: number; userId: number; products: number[]; total: number; } // models/index.ts - ملف البرميل export { User } from "./user"; export { Product } from "./product"; export { Order } from "./order"; // أو استخدم بناء جملة export * export * from "./user"; export * from "./product"; export * from "./order"; // main.ts - الاستيراد من البرميل import { User, Product, Order } from "./models"; const user: User = { id: 1, name: "Alice", email: "alice@example.com" }; const product: Product = { id: 1, name: "Laptop", price: 999 }; const order: Order = { id: 1, userId: 1, products: [1], total: 999 };
نصيحة: ملفات البراميل (index.ts) تبسط الاستيرادات بتوفير نقطة دخول واحدة لدليل. ومع ذلك، كن حذراً مع ملفات البراميل في المشاريع الكبيرة لأنها يمكن أن تؤثر على tree-shaking وحجم الحزمة.

استيرادات وصادرات الأنواع فقط

TypeScript يسمح باستيراد/تصدير الأنواع بشكل منفصل عن القيم:

// types.ts export interface User { id: number; name: string; } export type UserId = number; export class UserService { getUser(id: UserId): User | null { return null; } } // main.ts - استيرادات الأنواع فقط import type { User, UserId } from "./types"; import { UserService } from "./types"; // User و UserId أنواع فقط، تُحذف في وقت التشغيل const userId: UserId = 1; const user: User = { id: 1, name: "Alice" }; // UserService قيمة، مُضمنة في JS المُترجم const service = new UserService(); // صادرات الأنواع فقط // utils.ts interface Config { apiKey: string; endpoint: string; } export type { Config }; // صادرات مختلطة للأنواع والقيم export { type Config, UserService } from "./types"; // إعادة التصدير كنوع فقط export type { User as UserType } from "./types";
ملاحظة: استيرادات الأنواع فقط مفيدة بشكل خاص عندما تريد ضمان استخدام وحدة فقط لمعلومات النوع ويجب إزالتها تماماً من JavaScript المُترجم.

حل الوحدات (Module Resolution)

TypeScript يحتاج إلى فهم كيفية العثور على الوحدات. تكوين حل الوحدات في tsconfig.json:

// tsconfig.json { "compilerOptions": { // نظام الوحدات: "commonjs", "amd", "es2015", "es2020", "esnext", "node16" "module": "esnext", // استراتيجية حل الوحدات: "node" أو "classic" "moduleResolution": "node", // الدليل الأساسي لحل أسماء الوحدات غير النسبية "baseUrl": "./src", // تعيين المسار للأسماء المستعارة للوحدات "paths": { "@models/*": ["models/*"], "@utils/*": ["utils/*"], "@services/*": ["services/*"] }, // تضمين تعريفات الأنواع "types": ["node", "jest"], // السماح باستيراد ملفات .json "resolveJsonModule": true, // السماح بالاستيرادات الافتراضية من الوحدات بدون صادر افتراضي "allowSyntheticDefaultImports": true, // إصدار التشغيل البيني بين CommonJS و ES Modules "esModuleInterop": true } } // مع تعيين المسار، يمكنك استخدام: import { User } from "@models/user"; import { logger } from "@utils/logger"; // بدلاً من: import { User } from "../../models/user"; import { logger } from "../../utils/logger";

المساحات الاسمية (Namespaces)

المساحات الاسمية (كانت تُسمى سابقاً "الوحدات الداخلية") هي طريقة TypeScript القديمة لتنظيم الكود. لا تزال مفيدة لتنظيم السكريبتات العامة:

// تصريح مساحة اسمية namespace Validation { export interface StringValidator { isValid(s: string): boolean; } export class EmailValidator implements StringValidator { isValid(s: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s); } } export class UrlValidator implements StringValidator { isValid(s: string): boolean { try { new URL(s); return true; } catch { return false; } } } // مساعد خاص (غير مُصدّر) function log(message: string): void { console.log(`[Validation] ${message}`); } } // الاستخدام const emailValidator = new Validation.EmailValidator(); console.log(emailValidator.isValid("test@example.com")); // true console.log(emailValidator.isValid("invalid")); // false const urlValidator = new Validation.UrlValidator(); console.log(urlValidator.isValid("https://example.com")); // true console.log(urlValidator.isValid("not a url")); // false // مساحات اسمية متداخلة namespace Company { export namespace Employees { export class Manager { constructor(public name: string) {} } export class Developer { constructor(public name: string) {} } } export namespace Products { export interface Product { id: number; name: string; } } } const manager = new Company.Employees.Manager("Alice"); const developer = new Company.Employees.Developer("Bob");
تحذير: المساحات الاسمية تُعتبر قديمة. للكود الجديد، يُفضل وحدات ES. المساحات الاسمية لا تزال مفيدة لـ: تنظيم التصريحات المحيطة، والعمل مع السكريبتات العامة، وصيانة قواعد الكود القديمة.

أسماء مستعارة للمساحات الاسمية

أنشئ أسماء مستعارة أقصر للمساحات الاسمية المتداخلة بعمق:

namespace Shapes { export namespace Polygons { export class Triangle { constructor(public sides: number = 3) {} } export class Square { constructor(public sides: number = 4) {} } } } // اسم مستعار للوصول الأقصر import Polygons = Shapes.Polygons; const triangle = new Polygons.Triangle(); const square = new Polygons.Square(); console.log(triangle.sides); // 3 console.log(square.sides); // 4 // يمكن أيضاً إنشاء اسم مستعار لأصناف محددة import Triangle = Shapes.Polygons.Triangle; const tri = new Triangle();

دمج المساحات الاسمية (Namespace Merging)

المساحات الاسمية بنفس الاسم في نفس النطاق يتم دمجها تلقائياً:

// التصريح الأول namespace Animals { export class Dog { bark(): void { console.log("Woof!"); } } } // التصريح الثاني - مدمج مع الأول namespace Animals { export class Cat { meow(): void { console.log("Meow!"); } } } // كلا الصنفين متاحان const dog = new Animals.Dog(); const cat = new Animals.Cat(); dog.bark(); // "Woof!" cat.meow(); // "Meow!" // مفيد لتوسيع الوظائف namespace MathUtils { export function add(a: number, b: number): number { return a + b; } } namespace MathUtils { export function multiply(a: number, b: number): number { return a * b; } } console.log(MathUtils.add(2, 3)); // 5 console.log(MathUtils.multiply(2, 3)); // 6

دمج التصريحات (Declaration Merging)

TypeScript يسمح بدمج الواجهات، والمساحات الاسمية، وحتى الواجهات مع المساحات الاسمية:

// دمج الواجهات interface User { name: string; } interface User { email: string; } // كلا التصريحين مدمجان const user: User = { name: "Alice", email: "alice@example.com" }; // دمج الواجهة + المساحة الاسمية interface Album { label: string; } namespace Album { export class AlbumLabel { constructor(public name: string) {} } } // كل من الواجهة والمساحة الاسمية متاحة const album: Album = { label: "Rock" }; const albumLabel = new Album.AlbumLabel("Jazz Records"); // دمج الدالة + المساحة الاسمية function buildLabel(name: string): string { return name; } namespace buildLabel { export function fromObject(obj: { name: string }): string { return obj.name; } } console.log(buildLabel("Direct")); // "Direct" console.log(buildLabel.fromObject({ name: "From Object" })); // "From Object" // دمج Enum + المساحة الاسمية enum Color { Red, Green, Blue } namespace Color { export function mix(c1: Color, c2: Color): string { return `Mixed ${Color[c1]} and ${Color[c2]}`; } } console.log(Color.Red); // 0 console.log(Color.mix(Color.Red, Color.Blue)); // "Mixed Red and Blue"
ملاحظة: دمج التصريحات ميزة قوية تسمح لك بزيادة الأنواع الموجودة. يُستخدم هذا عادةً عند توسيع أنواع مكتبات الطرف الثالث أو إضافة دوال مساعدة إلى enums.

الوحدات المحيطة (Ambient Modules)

صرّح عن أنواع للوحدات التي ليس لديها تعريفات TypeScript:

// types.d.ts - تصريح وحدة محيطة declare module "legacy-library" { export function oldFunction(param: string): number; export interface OldInterface { prop: string; } export class OldClass { constructor(value: string); method(): void; } } // main.ts - استخدام المكتبة مع الأنواع import { oldFunction, OldInterface, OldClass } from "legacy-library"; const result = oldFunction("test"); // النوع: number const obj: OldInterface = { prop: "value" }; const instance = new OldClass("value"); // تصريحات وحدات بحرف بدل declare module "*.json" { const value: any; export default value; } declare module "*.css" { const classes: { [key: string]: string }; export default classes; } // الاستخدام import config from "./config.json"; // يعمل مع النوع any import styles from "./styles.css"; // يعمل مع قاموس نصوص

زيادة الوحدات (Module Augmentation)

قم بتوسيع الوحدات الموجودة بتصريحات إضافية:

// زيادة واجهة Array declare global { interface Array<T> { first(): T | undefined; last(): T | undefined; } } // التنفيذ Array.prototype.first = function() { return this[0]; }; Array.prototype.last = function() { return this[this.length - 1]; }; // الاستخدام const numbers = [1, 2, 3, 4, 5]; console.log(numbers.first()); // 1 console.log(numbers.last()); // 5 // زيادة وحدة خارجية // express.d.ts import "express"; declare module "express" { interface Request { user?: { id: number; name: string; }; } } // main.ts import express from "express"; const app = express(); app.get("/user", (req, res) => { // req.user الآن له نوع if (req.user) { res.json({ id: req.user.id, name: req.user.name }); } else { res.status(401).send("Unauthorized"); } });
تمرين: أنشئ نظام وحدات لتطبيق تجارة إلكترونية بسيط. ابنِ وحدات منفصلة لـ products, cart, و checkout. كل وحدة يجب أن تُصدّر واجهات وأصناف ودوال مساعدة. أنشئ ملف برميل (index.ts) يعيد تصدير كل شيء. استخدم تعيين المسار في tsconfig.json للاستيرادات النظيفة. أضف صادرات الأنواع فقط حيثما كان ذلك مناسباً. أخيراً، قم بزيادة واجهة Array بطريقة sum() لمصفوفات الأرقام.

الاختيار بين الوحدات والمساحات الاسمية

إليك متى تستخدم كل نهج:

  • استخدم وحدات ES عندما:
    • بناء تطبيقات حديثة (افتراضي موصى به)
    • العمل مع محزمات الوحدات (Webpack, Rollup, Vite)
    • تحتاج إلى tree-shaking وتقسيم الكود
    • العمل مع حزم npm
    • بناء مكتبات للتوزيع
  • استخدم المساحات الاسمية عندما:
    • العمل مع قواعد كود قديمة
    • كتابة سكريبتات عامة بدون نظام وحدات
    • تنظيم تصريحات الأنواع المحيطة
    • إنشاء تنظيم داخلي في ملفات التصريح

أفضل ممارسات الوحدات

  • احتفظ بالوحدات مُركزة: كل وحدة يجب أن يكون لها مسؤولية واحدة
  • تجنب التبعيات الدائرية: A يستورد B، B يستورد A ينشئ مشاكل
  • استخدم ملفات البراميل باعتدال: يمكن أن تؤثر على حجم الحزمة وأداء البناء
  • يُفضل الصادرات المسماة: الصادرات الافتراضية يمكن أن تؤدي إلى تسمية غير متسقة
  • استخدم تعيين المسار: تجنب مسارات استيراد نسبية طويلة مثل ../../../utils
  • صدّر الواجهات من التنفيذ: احتفظ بالأنواع قريبة من تنفيذاتها
  • وثق واجهات برمجية عامة: أضف تعليقات JSDoc للأعضاء المُصدّرة

الخلاصة

TypeScript يوفر أدوات تنظيم كود قوية من خلال وحدات ES والمساحات الاسمية. وحدات ES هي المعيار الحديث، تقدم تحليلاً ثابتاً، وtree-shaking، ودعم أدوات ممتاز. المساحات الاسمية تبقى مفيدة للكود القديم والتصريحات المحيطة. فهم حل الوحدات، ودمج التصريحات، وزيادة الوحدات يمكّنك من العمل بفعالية مع أي قاعدة كود TypeScript وإنشاء تطبيقات منظمة جيداً وقابلة للصيانة.