العربية

اكتشف قوة هياكل البيانات غير القابلة للتغيير في TypeScript باستخدام أنواع القراءة فقط. تعلم كيفية بناء تطبيقات أكثر قوة واستقرارًا وقابلية للصيانة من خلال منع التعديلات غير المقصودة على البيانات.

أنواع TypeScript للقراءة فقط: إتقان هياكل البيانات غير القابلة للتغيير

في المشهد المتطور باستمرار لتطوير البرمجيات، يعد السعي وراء كود برمجي قوي وقابل للتنبؤ والصيانة مسعى دائمًا. توفر TypeScript، بنظام أنواعها القوي، أدوات فعالة لتحقيق هذه الأهداف. ومن بين هذه الأدوات، تبرز أنواع القراءة فقط (readonly) كآلية حاسمة لفرض عدم القابلية للتغيير (immutability)، وهي حجر الزاوية في البرمجة الوظيفية ومفتاح لبناء تطبيقات أكثر موثوقية.

ما هي عدم القابلية للتغيير (Immutability) وما أهميتها؟

عدم القابلية للتغيير، في جوهرها، تعني أنه بمجرد إنشاء كائن ما، لا يمكن تغيير حالته. هذا المفهوم البسيط له آثار عميقة على جودة الكود وقابليته للصيانة.

أنواع القراءة فقط في TypeScript: ترسانتك لعدم القابلية للتغيير

توفر TypeScript عدة طرق لفرض عدم القابلية للتغيير باستخدام الكلمة المفتاحية readonly. دعنا نستكشف التقنيات المختلفة وكيف يمكن تطبيقها عمليًا.

1. خصائص القراءة فقط (Readonly) في الواجهات والأنواع

الطريقة الأكثر مباشرة للإعلان عن خاصية كقراءة فقط هي استخدام الكلمة المفتاحية readonly مباشرة في تعريف واجهة أو نوع.


interface Person {
  readonly id: string;
  name: string;
  age: number;
}

const person: Person = {
  id: "unique-id-123",
  name: "Alice",
  age: 30,
};

// person.id = "new-id"; // خطأ: لا يمكن الإسناد إلى 'id' لأنها خاصية للقراءة فقط.
person.name = "Bob"; // هذا مسموح به

في هذا المثال، تم الإعلان عن خاصية id كـ readonly. ستمنع TypeScript أي محاولات لتعديلها بعد إنشاء الكائن. أما خاصيتا name وage، اللتان تفتقران إلى مُعدِّل readonly، فيمكن تعديلهما بحرية.

2. النوع المساعد Readonly

تقدم TypeScript نوعًا مساعدًا قويًا يسمى Readonly<T>. يأخذ هذا النوع العام نوعًا موجودًا T ويحوله عن طريق جعل جميع خصائصه readonly.


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

const point: Readonly<Point> = {
  x: 10,
  y: 20,
};

// point.x = 30; // خطأ: لا يمكن الإسناد إلى 'x' لأنها خاصية للقراءة فقط.

النوع Readonly<Point> ينشئ نوعًا جديدًا حيث تكون كل من x وy للقراءة فقط. هذه طريقة ملائمة لجعل نوع موجود غير قابل للتغيير بسرعة.

3. المصفوفات للقراءة فقط (ReadonlyArray<T>) وreadonly T[]

المصفوفات في JavaScript قابلة للتغيير بطبيعتها. توفر TypeScript طريقة لإنشاء مصفوفات للقراءة فقط باستخدام النوع ReadonlyArray<T> أو الاختصار readonly T[]. هذا يمنع تعديل محتويات المصفوفة.


const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];
// numbers.push(6); // خطأ: الخاصية 'push' غير موجودة في النوع 'readonly number[]'.
// numbers[0] = 10; // خطأ: توقيع الفهرس في النوع 'readonly number[]' يسمح بالقراءة فقط.

const moreNumbers: readonly number[] = [6, 7, 8, 9, 10]; // مكافئ لـ ReadonlyArray
// moreNumbers.push(11); // خطأ: الخاصية 'push' غير موجودة في النوع 'readonly number[]'.

محاولة استخدام طرق تعدل المصفوفة، مثل push، pop، splice، أو الإسناد المباشر إلى فهرس، ستؤدي إلى خطأ في TypeScript.

4. const مقابل readonly: فهم الفرق

من المهم التمييز بين const وreadonly. تمنع const إعادة إسناد المتغير نفسه، بينما تمنع readonly تعديل خصائص الكائن. إنهما يخدمان أغراضًا مختلفة ويمكن استخدامهما معًا لتحقيق أقصى قدر من عدم القابلية للتغيير.


const immutableNumber = 42;
// immutableNumber = 43; // خطأ: لا يمكن إعادة الإسناد إلى متغير const 'immutableNumber'.

const mutableObject = { value: 10 };
mutableObject.value = 20; // هذا مسموح به لأن *الكائن* ليس ثابتًا، بل المتغير فقط.

const readonlyObject: Readonly<{ value: number }> = { value: 30 };
// readonlyObject.value = 40; // خطأ: لا يمكن الإسناد إلى 'value' لأنها خاصية للقراءة فقط.

const constReadonlyObject: Readonly<{ value: number }> = { value: 50 };
// constReadonlyObject = { value: 60 }; // خطأ: لا يمكن إعادة الإسناد إلى متغير const 'constReadonlyObject'.
// constReadonlyObject.value = 60; // خطأ: لا يمكن الإسناد إلى 'value' لأنها خاصية للقراءة فقط.

كما هو موضح أعلاه، تضمن const أن المتغير يشير دائمًا إلى نفس الكائن في الذاكرة، بينما تضمن readonly أن الحالة الداخلية للكائن تظل دون تغيير.

أمثلة عملية: تطبيق أنواع القراءة فقط في سيناريوهات واقعية

دعنا نستكشف بعض الأمثلة العملية لكيفية استخدام أنواع القراءة فقط لتعزيز جودة الكود وقابليته للصيانة في سيناريوهات مختلفة.

1. إدارة بيانات الإعدادات

غالبًا ما يتم تحميل بيانات الإعدادات مرة واحدة عند بدء تشغيل التطبيق ويجب عدم تعديلها أثناء وقت التشغيل. يضمن استخدام أنواع القراءة فقط أن هذه البيانات تظل متسقة ويمنع التعديلات العرضية.


interface AppConfig {
  readonly apiUrl: string;
  readonly timeout: number;
  readonly features: readonly string[];
}

const config: AppConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  features: ["featureA", "featureB"],
};

function fetchData(url: string, config: Readonly<AppConfig>) {
    // ... استخدم config.timeout و config.apiUrl بأمان، مع العلم أنها لن تتغير
}

fetchData("/data", config);

2. تطبيق إدارة الحالة الشبيهة بـ Redux

في مكتبات إدارة الحالة مثل Redux، تعد عدم القابلية للتغيير مبدأً أساسيًا. يمكن استخدام أنواع القراءة فقط لضمان بقاء الحالة غير قابلة للتغيير وأن المُختزِلات (reducers) تعيد فقط كائنات حالة جديدة بدلاً من تعديل الكائنات الموجودة.


interface State {
  readonly count: number;
  readonly items: readonly string[];
}

const initialState: State = {
  count: 0,
  items: [],
};

function reducer(state: Readonly<State>, action: { type: string; payload?: any }): State {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 }; // إرجاع كائن حالة جديد
    case "ADD_ITEM":
      return { ...state, items: [...state.items, action.payload] }; // إرجاع كائن حالة جديد مع عناصر محدثة
    default:
      return state;
  }
}

3. التعامل مع استجابات الواجهة البرمجية (API)

عند جلب البيانات من واجهة برمجية (API)، غالبًا ما يكون من المرغوب فيه التعامل مع بيانات الاستجابة على أنها غير قابلة للتغيير، خاصة إذا كنت تستخدمها لعرض مكونات واجهة المستخدم. يمكن أن تساعد أنواع القراءة فقط في منع التعديلات العرضية لبيانات الـ API.


interface ApiResponse {
  readonly userId: number;
  readonly id: number;
  readonly title: string;
  readonly completed: boolean;
}

async function fetchTodo(id: number): Promise<Readonly<ApiResponse>> {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
  const data: ApiResponse = await response.json();
  return data;
}

fetchTodo(1).then(todo => {
  console.log(todo.title);
  // todo.completed = true; // خطأ: لا يمكن الإسناد إلى 'completed' لأنها خاصية للقراءة فقط.
});

4. نمذجة البيانات الجغرافية (مثال دولي)

لنفترض أننا نمثل إحداثيات جغرافية. بمجرد تعيين إحداثي، يجب أن يظل ثابتًا بشكل مثالي. هذا يضمن سلامة البيانات، خاصة عند التعامل مع التطبيقات الحساسة مثل أنظمة الخرائط أو الملاحة التي تعمل عبر مناطق جغرافية مختلفة (على سبيل المثال، إحداثيات GPS لخدمة توصيل تمتد عبر أمريكا الشمالية وأوروبا وآسيا).


interface GeoCoordinates {
 readonly latitude: number;
 readonly longitude: number;
}

const tokyoCoordinates: GeoCoordinates = {
 latitude: 35.6895,
 longitude: 139.6917
};

const newYorkCoordinates: GeoCoordinates = {
 latitude: 40.7128,
 longitude: -74.0060
};


function calculateDistance(coord1: Readonly<GeoCoordinates>, coord2: Readonly<GeoCoordinates>): number {
 // تخيل عملية حسابية معقدة باستخدام خطوط الطول والعرض
 // إرجاع قيمة وهمية للتبسيط
 return 1000; 
}

const distance = calculateDistance(tokyoCoordinates, newYorkCoordinates);
console.log("المسافة بين طوكيو ونيويورك (قيمة وهمية):", distance);

// tokyoCoordinates.latitude = 36.0; // خطأ: لا يمكن الإسناد إلى 'latitude' لأنها خاصية للقراءة فقط.

أنواع القراءة فقط العميقة: التعامل مع الكائنات المتداخلة

النوع المساعد Readonly<T> يجعل فقط الخصائص المباشرة للكائن readonly. إذا كان الكائن يحتوي على كائنات أو مصفوفات متداخلة، فإن تلك الهياكل المتداخلة تظل قابلة للتغيير. لتحقيق عدم قابلية التغيير العميقة الحقيقية، تحتاج إلى تطبيق Readonly<T> بشكل متكرر على جميع الخصائص المتداخلة.

إليك مثال على كيفية إنشاء نوع للقراءة فقط بشكل عميق:


type DeepReadonly<T> = T extends (infer R)[]
  ? DeepReadonlyArray<R>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
  };
  employees: string[];
}

const company: DeepReadonly<Company> = {
  name: "Example Corp",
  address: {
    street: "123 Main St",
    city: "Anytown",
    country: "USA",
  },
  employees: ["Alice", "Bob"],
};

// company.name = "New Corp"; // خطأ
// company.address.city = "New City"; // خطأ
// company.employees.push("Charlie"); // خطأ

يطبق هذا النوع DeepReadonly<T> بشكل متكرر Readonly<T> على جميع الخصائص المتداخلة، مما يضمن أن بنية الكائن بأكملها غير قابلة للتغيير.

الاعتبارات والمقايضات

بينما توفر عدم القابلية للتغيير فوائد كبيرة، من المهم أن تكون على دراية بالمقايضات المحتملة.

مكتبات لهياكل البيانات غير القابلة للتغيير

يمكن لعدة مكتبات تبسيط العمل مع هياكل البيانات غير القابلة للتغيير في TypeScript:

أفضل الممارسات لاستخدام أنواع القراءة فقط

للاستفادة بشكل فعال من أنواع القراءة فقط في مشاريع TypeScript الخاصة بك، اتبع أفضل الممارسات التالية:

الخاتمة: تبني عدم القابلية للتغيير مع أنواع القراءة فقط في TypeScript

تُعد أنواع القراءة فقط في TypeScript أداة قوية لبناء تطبيقات أكثر قابلية للتنبؤ والصيانة والقوة. من خلال تبني عدم القابلية للتغيير، يمكنك تقليل مخاطر الأخطاء، وتبسيط عملية تصحيح الأخطاء، وتحسين الجودة الإجمالية للكود الخاص بك. على الرغم من وجود بعض المقايضات التي يجب مراعاتها، إلا أن فوائد عدم القابلية للتغيير غالبًا ما تفوق التكاليف، خاصة في المشاريع المعقدة وطويلة الأمد. بينما تواصل رحلتك مع TypeScript، اجعل أنواع القراءة فقط جزءًا أساسيًا من سير عملك التطويري لإطلاق العنان للإمكانات الكاملة لعدم القابلية للتغيير وبناء برامج موثوقة حقًا.