يعلّم readonly خاصية يمكن تعيينها مرة واحدة، عند إنشاء الكائن، ولا يُعاد إسنادها أبدًا. ويطبّقه Readonly<T> على كل خصائص نوع، ويفعل readonly T[] الشيء نفسه للمصفوفات:
يُظهر السطر الأخير أهم حقيقة عن readonly: يفحصه المترجم، ولا يُفرض وقت التشغيل. كان الإسناد خطأ ترجمة (كُتم هنا بـ @ts-expect-error)، ومع ذلك نفّذه JavaScript المولَّد. من دون الكتم لن يُترجم الملف، وهنا يؤدي readonly وظيفته.
خصائص readonly
ضع readonly قبل اسم الخاصية في interface أو نوع كائن حرفي أو صنف. يمكن تهيئة الخاصية لكن لا يمكن إعادة إسنادها:
في الصنف، يمكن إسناد حقل readonly في تعريفه أو في المُنشئ، ولا مكان غيرهما. والشكل الأقصر هو خاصية المعامل (parameter property)، constructor(readonly id: string) {}، التي تعرّف الحقل وتسنده في خطوة واحدة. تشرح صفحة الأصناف الحقول والمُنشئات عمومًا.
Readonly<T>: كل الخصائص دفعة واحدة
Readonly<T> نوع أداة يعلّم كل خصائص T كـ readonly. وهو مفيد للقيم التي تمررها ولا يجب أن تتغير، مثل حالة التطبيق:
يخبر توقيع الدالة القارئ أن addItem تعيد حالة جديدة بدلًا من تغيير القديمة، ويُلزم المترجم الدالة بذلك. ويُعرَّف Readonly<T> كـ mapped type: { readonly [P in keyof T]: T[P] }.
المصفوفات للقراءة فقط: readonly T[] وReadonlyArray<T>
readonly number[] وReadonlyArray<number> النوع نفسه. يزيلان كل دالة معدِّلة (push وpop وshift وsplice وsort وreverse وfill...) ويمنعان الإسناد عبر الفهرس. وتبقى الدوال غير المعدِّلة وتعيد مصفوفات عادية:
أخذ readonly T[] كمعامل وعدٌ للمستدعين بأنك لن تعدّل مصفوفتهم. أما الحالة المعاكسة فهي حيث يتعثر الناس: لا يمكن تمرير مصفوفة للقراءة فقط إلى دالة تأخذ T[] عاديًا، لأن تلك الدالة قد تعدّلها.
index.ts(7,17): error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
الحل هو تغيير sum لتقبل readonly number[]، لأنها لا تعدّل شيئًا. الدوال التي تقرأ المصفوفة فقط يجب أن تأخذ دائمًا النوع للقراءة فقط؛ وعندها تقبل النوعين. وإذا لم تكن الدالة ملكك، فمرّر نسخة: sum([...prices]).
ReadonlyMap وReadonlySet
للـ Maps والـ sets نسخ للقراءة فقط أيضًا. في ReadonlyMap<K, V> توجد get وhas وsize وforEach والمكرِّرات لكن لا توجد set ولا delete ولا clear؛ وفي ReadonlySet<T> لا توجد add ولا delete ولا clear:
كثيرًا ما يحتفظ الصنف بـ Map خاص قابل للتعديل ويكشفه عبر getter نوعه ReadonlyMap، فيستطيع الكود الخارجي قراءة البيانات دون تغييرها عبر ذلك المرجع.
readonly سطحي
يحمي readonly وReadonly<T> الخاصية نفسها فقط، لا الكائن أو المصفوفة التي تشير إليها:
يطبّق DeepReadonly<T> نفسه على كل نوع كائن متداخل، ولأن الـ mapped type على نوع مصفوفة ينتج مصفوفة للقراءة فقط، يصبح members من النوع readonly string[]. ويبقى ذلك وعدًا على مستوى الأنواع، لا حماية وقت التشغيل.
وقت الترجمة فقط: التعديل عبر مرجع آخر
النوع للقراءة فقط يتحكم فيما يُسمح لمرجع واحد بفعله. ومرجع آخر إلى الكائن نفسه، نوعه دون readonly، يستطيع تغييره، بل تسمح TypeScript بإسناد نوع للقراءة فقط إلى نوع قابل للتعديل:
يُترجم الإسناد mutable = settings لأن TypeScript لا تأخذ خصائص readonly في الحسبان عندما تتحقق من توافق نوعي كائن؛ يقول دليل TypeScript ذلك صراحةً، ويذكر أن خصائص readonly يمكن بالتالي أن تتغير عبر الأسماء المستعارة (aliasing). المصفوفات للقراءة فقط مختلفة: الخطأ TS4104 أعلاه هو هذا الفحص تحديدًا. أما Object.freeze فيمنع التغييرات فعلًا وقت التشغيل: يعمل الكود المولَّد في الوضع الصارم (strict mode)، حيث ترمي الكتابة إلى خاصية مجمّدة TypeError. ومثل readonly فإن Object.freeze سطحي.
readonly وconst وas const وObject.freeze
يجعل as const على قيمة حرفية كل خاصية readonly في كل عمق ويحافظ على الأنواع الحرفية، وكثيرًا ما يكون أسهل طريقة للحصول على قيمة للقراءة فقط بعمق:
const theme = { mode: "dark", sizes: [12, 14] } as const;
// { readonly mode: "dark"; readonly sizes: readonly [12, 14] }
| يُطبَّق على | عميق؟ | الأثر وقت التشغيل | مثال | |
|---|---|---|---|---|
const | ربط متغير | لا | لا يمكن إعادة إسناد المتغير | const user = {...} |
readonly | خاصية واحدة أو نوع مصفوفة | لا | لا شيء | readonly id: string |
Readonly<T> | كل خصائص نوع | لا | لا شيء | Readonly<State> |
as const | تعبير حرفي | نعم | لا شيء | { ... } as const |
Object.freeze | قيمة كائن | لا | الكتابة تفشل (وترمي خطأ في الوضع الصارم) | Object.freeze(obj) |
يجيب const وreadonly عن سؤالين مختلفين: يمنع const الاسم من الإشارة إلى مكان آخر، ويمنع readonly الخاصية من التغير. ويمكن إعادة إسناد خصائص كائن const ما لم تكن readonly.
الأسئلة الشائعة
ماذا يفعل readonly في TypeScript؟
يعلّم readonly خاصية يمكن تعيينها عند إنشاء الكائن (أو في مُنشئ الصنف) لكن لا يمكن إعادة إسنادها بعد ذلك. الإسناد إليها لاحقًا خطأ ترجمة، TS2540. إنه فحص أنواع فقط: JavaScript المولَّد لا يحتوي أي حماية.
ما الفرق بين readonly وconst في TypeScript؟
يتعلق const بالمتغير: لا يمكن توجيه الاسم إلى قيمة أخرى، لكن الكائن الذي يحمله يمكن تغييره. أما readonly فيتعلق بالخاصية: لا يمكن إعادة إسناد تلك الخاصية. const user = { name: "Ada" } يسمح مع ذلك بـ user.name = "x"؛ أما الخاصية readonly name فلا تسمح بذلك.
كيف أجعل مصفوفة للقراءة فقط في TypeScript؟
صرّح بنوعها كـ readonly T[] أو ReadonlyArray<T> (النوع نفسه). تختفي من النوع الدوال المعدِّلة مثل push وpop وsort وsplice، ويصبح الإسناد عبر الفهرس خطأ. أما الدوال غير المعدِّلة مثل map وfilter وslice فتبقى وتعيد مصفوفات عادية.
هل Readonly عميق في TypeScript؟
لا. يحمي Readonly<T> وreadonly خصائص المستوى الأعلى فقط؛ أما الكائنات والمصفوفات المتداخلة داخلها فيمكن تغييرها. استخدم as const على قيمة حرفية، أو اكتب نوعًا عوديًا DeepReadonly<T>، لحماية عميقة على مستوى الأنواع.
هل يمنع readonly التغييرات وقت التشغيل؟
لا. تُحذف الأنواع، فالخاصية readonly خاصية عادية وقت التشغيل، والكود الذي يملك مرجعًا قابلًا للتعديل إلى الكائن نفسه (أو JavaScript عادي) يستطيع تغييرها. استخدم Object.freeze عندما تحتاج إلى حماية وقت التشغيل؛ وتعطي TypeScript نتيجته النوع Readonly<T>.